Compare commits
8 commits
joyful-0.0
...
main
Author | SHA1 | Date | |
---|---|---|---|
8a903e0703 | |||
8d2b15a7c8 | |||
d9babf5dc0 | |||
1a7b288083 | |||
7a9a2ba9e2 | |||
329058b4b5 | |||
890c19f1dc | |||
838449000c |
52 changed files with 2007 additions and 1429 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
build/
|
||||
build/
|
||||
target/
|
|
@ -5,7 +5,8 @@ import (
|
|||
"slices"
|
||||
|
||||
// TODO: using config here feels like bad coupling... ButtonFromIndex might need a refactor / move
|
||||
"git.annabunches.net/annabunches/joyful/internal/config"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
flag "github.com/spf13/pflag"
|
||||
|
@ -20,7 +21,7 @@ func isJoystickLike(device *evdev.InputDevice) bool {
|
|||
if slices.Contains(types, evdev.EV_KEY) {
|
||||
buttons := device.CapableEvents(evdev.EV_KEY)
|
||||
|
||||
for _, code := range config.ButtonFromIndex {
|
||||
for _, code := range eventcodes.ButtonFromIndex {
|
||||
if slices.Contains(buttons, code) {
|
||||
return true
|
||||
}
|
||||
|
@ -43,6 +44,32 @@ func printDevice(devPath evdev.InputPath) {
|
|||
return
|
||||
}
|
||||
|
||||
// Get metadata
|
||||
// metadata := struct {
|
||||
// uuid string
|
||||
// vendor string
|
||||
// product string
|
||||
// version string
|
||||
// }{}
|
||||
|
||||
// uuid, err := device.UniqueID()
|
||||
// if err != nil {
|
||||
// metadata.uuid = "unknown"
|
||||
// } else {
|
||||
// metadata.uuid = uuid
|
||||
// }
|
||||
|
||||
// inputId, err := device.InputID()
|
||||
// if err != nil {
|
||||
// metadata.vendor = "unknown"
|
||||
// metadata.product = "unknown"
|
||||
// metadata.version = "unknown"
|
||||
// } else {
|
||||
// metadata.vendor = "0x" + strconv.FormatUint(uint64(inputId.Vendor), 16)
|
||||
// metadata.product = "0x" + strconv.FormatUint(uint64(inputId.Product), 16)
|
||||
// metadata.version = strconv.FormatUint(uint64(inputId.Version), 10)
|
||||
// }
|
||||
|
||||
// Get axis info
|
||||
var axisOutputs []string
|
||||
absInfos, err := device.AbsInfos()
|
||||
|
@ -56,8 +83,13 @@ func printDevice(devPath evdev.InputPath) {
|
|||
}
|
||||
}
|
||||
|
||||
// Print everything
|
||||
fmt.Printf("%s:\n", devPath.Path)
|
||||
fmt.Printf("\tName: '%s'\n", devPath.Name)
|
||||
fmt.Printf("\tName:\t\t'%s'\n", devPath.Name)
|
||||
// fmt.Printf("\tUUID:\t\t'%s'\n", metadata.uuid)
|
||||
// fmt.Printf("\tVendor:\t\t'%s'\n", metadata.vendor)
|
||||
// fmt.Printf("\tProduct:\t'%s'\n", metadata.product)
|
||||
// fmt.Printf("\tVersion:\t'%s'\n", metadata.version)
|
||||
if len(axisOutputs) > 0 {
|
||||
fmt.Println("\tAxes:")
|
||||
for _, str := range axisOutputs {
|
||||
|
@ -76,7 +108,7 @@ func printDeviceQuiet(devPath evdev.InputPath) {
|
|||
return
|
||||
}
|
||||
|
||||
fmt.Printf("'%s'\n", devPath.Name)
|
||||
fmt.Printf("'%s': '%s'\n", devPath.Path, devPath.Name)
|
||||
}
|
||||
|
||||
// TODO: it would be nice to be able to specify a device by name or device file and get axis info
|
||||
|
|
146
cmd/joyful/config.go
Normal file
146
cmd/joyful/config.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
|
||||
"git.annabunches.net/annabunches/joyful/internal/virtualdevice"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
func initPhysicalDevices(conf *configparser.Config) map[string]*evdev.InputDevice {
|
||||
pDeviceMap := make(map[string]*evdev.InputDevice)
|
||||
|
||||
for _, devConfig := range conf.Devices {
|
||||
if devConfig.Type != configparser.DeviceTypePhysical {
|
||||
continue
|
||||
}
|
||||
|
||||
innerConfig := devConfig.Config.(configparser.DeviceConfigPhysical)
|
||||
name, device, err := initPhysicalDevice(innerConfig)
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to initialize physical device")
|
||||
continue
|
||||
}
|
||||
|
||||
pDeviceMap[name] = device
|
||||
|
||||
displayName := innerConfig.DeviceName
|
||||
if innerConfig.DevicePath != "" {
|
||||
displayName = innerConfig.DevicePath
|
||||
}
|
||||
logger.Logf("Connected to '%s' as '%s'", displayName, name)
|
||||
}
|
||||
|
||||
if len(pDeviceMap) == 0 {
|
||||
logger.Log("Warning: no physical devices found in configuration. No rules will work.")
|
||||
}
|
||||
return pDeviceMap
|
||||
}
|
||||
|
||||
func initPhysicalDevice(config configparser.DeviceConfigPhysical) (string, *evdev.InputDevice, error) {
|
||||
name := config.Name
|
||||
var device *evdev.InputDevice
|
||||
var err error
|
||||
|
||||
if config.DevicePath != "" {
|
||||
device, err = evdev.Open(config.DevicePath)
|
||||
} else {
|
||||
device, err = evdev.OpenByName(config.DeviceName)
|
||||
}
|
||||
|
||||
if config.Lock && err == nil {
|
||||
grabErr := device.Grab()
|
||||
logger.LogIfError(grabErr, "Failed to lock device for exclusive access")
|
||||
}
|
||||
|
||||
return name, device, err
|
||||
}
|
||||
|
||||
// TODO: juggling all these maps is a pain. Is there a better solution here?
|
||||
func initVirtualBuffers(config *configparser.Config) (map[string]*evdev.InputDevice,
|
||||
map[string]*virtualdevice.EventBuffer,
|
||||
map[*evdev.InputDevice]*virtualdevice.EventBuffer) {
|
||||
|
||||
vDevicesByName := make(map[string]*evdev.InputDevice)
|
||||
vBuffersByName := make(map[string]*virtualdevice.EventBuffer)
|
||||
vBuffersByDevice := make(map[*evdev.InputDevice]*virtualdevice.EventBuffer)
|
||||
|
||||
for _, devConfig := range config.Devices {
|
||||
if devConfig.Type != configparser.DeviceTypeVirtual {
|
||||
continue
|
||||
}
|
||||
|
||||
vConfig := devConfig.Config.(configparser.DeviceConfigVirtual)
|
||||
buffer, err := virtualdevice.NewEventBuffer(vConfig)
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create virtual device, skipping")
|
||||
continue
|
||||
}
|
||||
vDevicesByName[buffer.Name] = buffer.Device.(*evdev.InputDevice)
|
||||
vBuffersByName[buffer.Name] = buffer
|
||||
vBuffersByDevice[buffer.Device.(*evdev.InputDevice)] = buffer
|
||||
}
|
||||
|
||||
if len(vDevicesByName) == 0 {
|
||||
logger.Log("Warning: no virtual devices found in configuration. No rules will work.")
|
||||
}
|
||||
|
||||
return vDevicesByName, vBuffersByName, vBuffersByDevice
|
||||
}
|
||||
|
||||
// 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[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 loadRules(
|
||||
config *configparser.Config,
|
||||
pDevices map[string]*evdev.InputDevice,
|
||||
vDevices map[string]*evdev.InputDevice,
|
||||
modes []string) ([]mappingrules.MappingRule, <-chan ChannelEvent, func(), *sync.WaitGroup) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
eventChannel := make(chan ChannelEvent, 1000)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Setup device mapping for the mappingrules package
|
||||
pDevs := mappingrules.ConvertDeviceMap(pDevices)
|
||||
vDevs := mappingrules.ConvertDeviceMap(vDevices)
|
||||
|
||||
// Initialize rules
|
||||
rules := make([]mappingrules.MappingRule, 0)
|
||||
for _, ruleConfig := range config.Rules {
|
||||
newRule, err := mappingrules.NewRule(ruleConfig, pDevs, vDevs, modes)
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create rule, skipping")
|
||||
continue
|
||||
}
|
||||
rules = append(rules, newRule)
|
||||
}
|
||||
|
||||
logger.Logf("Created %d mapping rules.", len(rules))
|
||||
|
||||
// start listening for events on devices and timers
|
||||
for _, device := range pDevices {
|
||||
wg.Add(1)
|
||||
go eventWatcher(device, eventChannel, ctx, &wg)
|
||||
}
|
||||
|
||||
timerCount := 0
|
||||
for _, rule := range rules {
|
||||
if timedRule, ok := rule.(mappingrules.TimedEventEmitter); ok {
|
||||
wg.Add(1)
|
||||
go timerWatcher(timedRule, eventChannel, ctx, &wg)
|
||||
timerCount++
|
||||
}
|
||||
}
|
||||
logger.Logf("Registered %d timers.", timerCount)
|
||||
|
||||
go consoleWatcher(eventChannel)
|
||||
|
||||
return rules, eventChannel, cancel, &wg
|
||||
}
|
|
@ -1,19 +1,15 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/holoplot/go-evdev"
|
||||
flag "github.com/spf13/pflag"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/config"
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
|
||||
"git.annabunches.net/annabunches/joyful/internal/virtualdevice"
|
||||
)
|
||||
|
||||
func getConfigDir(dir string) string {
|
||||
|
@ -21,83 +17,49 @@ func getConfigDir(dir string) string {
|
|||
return os.ExpandEnv(configDir)
|
||||
}
|
||||
|
||||
func readConfig(configDir string) *config.ConfigParser {
|
||||
parser := &config.ConfigParser{}
|
||||
err := parser.Parse(configDir)
|
||||
logger.FatalIfError(err, "Failed to parse config")
|
||||
return parser
|
||||
}
|
||||
|
||||
func initVirtualBuffers(config *config.ConfigParser) (map[string]*virtualdevice.EventBuffer, map[*evdev.InputDevice]*virtualdevice.EventBuffer) {
|
||||
vDevices := config.CreateVirtualDevices()
|
||||
if len(vDevices) == 0 {
|
||||
logger.Log("Warning: no virtual devices found in configuration. No rules will work.")
|
||||
}
|
||||
|
||||
vBuffersByName := make(map[string]*virtualdevice.EventBuffer)
|
||||
vBuffersByDevice := make(map[*evdev.InputDevice]*virtualdevice.EventBuffer)
|
||||
for name, device := range vDevices {
|
||||
vBuffersByName[name] = virtualdevice.NewEventBuffer(device)
|
||||
vBuffersByDevice[device] = vBuffersByName[name]
|
||||
}
|
||||
return vBuffersByName, vBuffersByDevice
|
||||
}
|
||||
|
||||
// Extracts the evdev devices from a list of virtual buffers and returns them.
|
||||
func getVirtualDevices(buffers map[string]*virtualdevice.EventBuffer) map[string]*evdev.InputDevice {
|
||||
devices := make(map[string]*evdev.InputDevice)
|
||||
for name, buffer := range buffers {
|
||||
devices[name] = buffer.Device.(*evdev.InputDevice)
|
||||
}
|
||||
return devices
|
||||
}
|
||||
|
||||
func initPhysicalDevices(config *config.ConfigParser, lock bool) map[string]*evdev.InputDevice {
|
||||
pDeviceMap := config.ConnectPhysicalDevices(lock)
|
||||
if len(pDeviceMap) == 0 {
|
||||
logger.Log("Warning: no physical devices found in configuration. No rules will work.")
|
||||
}
|
||||
return pDeviceMap
|
||||
}
|
||||
|
||||
func main() {
|
||||
// parse command-line
|
||||
var configFlag string
|
||||
var noLockFlag bool
|
||||
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.")
|
||||
ttsOps := addTTSFlags()
|
||||
flag.Parse()
|
||||
|
||||
// parse configs
|
||||
configDir := getConfigDir(configFlag)
|
||||
config := readConfig(configDir)
|
||||
config, err := configparser.ParseConfig(configDir)
|
||||
logger.FatalIfError(err, "Failed to parse configuration")
|
||||
|
||||
// initialize TTS
|
||||
tts, err := newTTS(ttsOps)
|
||||
logger.LogIfError(err, "Failed to initialize TTS")
|
||||
|
||||
// Initialize virtual devices with event buffers
|
||||
vBuffersByName, vBuffersByDevice := initVirtualBuffers(config)
|
||||
vDevicesByName, vBuffersByName, vBuffersByDevice := initVirtualBuffers(config)
|
||||
|
||||
// Initialize physical devices
|
||||
pDevices := initPhysicalDevices(config, !noLockFlag)
|
||||
pDevices := initPhysicalDevices(config)
|
||||
|
||||
// initialize the mode variables
|
||||
var mode string
|
||||
modes := config.Modes
|
||||
if len(modes) == 0 {
|
||||
mode = "*"
|
||||
} else {
|
||||
mode = config.Modes[0]
|
||||
}
|
||||
|
||||
// Load the rules
|
||||
rules, eventChannel, cancel, wg := loadRules(config, pDevices, getVirtualDevices(vBuffersByName))
|
||||
|
||||
// initialize the mode variable
|
||||
mode := config.GetModes()[0]
|
||||
rules, eventChannel, cancel, wg := loadRules(config, pDevices, vDevicesByName, modes)
|
||||
|
||||
// initialize TTS phrases for modes
|
||||
for _, m := range config.GetModes() {
|
||||
for _, m := range modes {
|
||||
tts.AddMessage(m)
|
||||
logger.LogDebugf("Added TTS message '%s'", m)
|
||||
}
|
||||
|
||||
fmt.Println("Joyful Running! Press Ctrl+C to quit. Press Enter to reload rules.")
|
||||
if len(config.GetModes()) > 1 {
|
||||
if len(modes) > 0 {
|
||||
logger.Logf("Initial mode set to '%s'", mode)
|
||||
}
|
||||
|
||||
|
@ -135,13 +97,18 @@ func main() {
|
|||
|
||||
case ChannelEventReload:
|
||||
// stop existing channels
|
||||
config, err := configparser.ParseConfig(configDir) // reload the config
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to parse config, no changes made")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Println("Reloading rules.")
|
||||
cancel()
|
||||
fmt.Println("Waiting for existing listeners to exit. Provide input from each of your devices.")
|
||||
wg.Wait()
|
||||
fmt.Println("Listeners exited. Parsing config.")
|
||||
config := readConfig(configDir) // reload the config
|
||||
rules, eventChannel, cancel, wg = loadRules(config, pDevices, getVirtualDevices(vBuffersByName))
|
||||
fmt.Println("Listeners exited. Loading new rules.")
|
||||
rules, eventChannel, cancel, wg = loadRules(config, pDevices, vDevicesByName, modes)
|
||||
fmt.Println("Config re-loaded. Only rule changes applied. Device and Mode changes require restart.")
|
||||
}
|
||||
|
||||
|
@ -150,37 +117,3 @@ func main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadRules(
|
||||
config *config.ConfigParser,
|
||||
pDevices map[string]*evdev.InputDevice,
|
||||
vDevices map[string]*evdev.InputDevice) ([]mappingrules.MappingRule, <-chan ChannelEvent, func(), *sync.WaitGroup) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
eventChannel := make(chan ChannelEvent, 1000)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Initialize rules
|
||||
rules := config.BuildRules(pDevices, vDevices)
|
||||
logger.Logf("Created %d mapping rules.", len(rules))
|
||||
|
||||
// start listening for events on devices and timers
|
||||
for _, device := range pDevices {
|
||||
wg.Add(1)
|
||||
go eventWatcher(device, eventChannel, ctx, &wg)
|
||||
}
|
||||
|
||||
timerCount := 0
|
||||
for _, rule := range rules {
|
||||
if timedRule, ok := rule.(mappingrules.TimedEventEmitter); ok {
|
||||
wg.Add(1)
|
||||
go timerWatcher(timedRule, eventChannel, ctx, &wg)
|
||||
timerCount++
|
||||
}
|
||||
}
|
||||
logger.Logf("Registered %d timers.", timerCount)
|
||||
|
||||
go consoleWatcher(eventChannel)
|
||||
|
||||
return rules, eventChannel, cancel, &wg
|
||||
}
|
||||
|
|
|
@ -1,19 +1,7 @@
|
|||
devices:
|
||||
- name: primary
|
||||
type: virtual
|
||||
num_axes: 6
|
||||
buttons:
|
||||
- BTN_EAST
|
||||
- BTN_SOUTH
|
||||
- BTN_NORTH
|
||||
- BTN_WEST
|
||||
- BTN_TL
|
||||
- BTN_TR
|
||||
- BTN_SELECT
|
||||
- BTN_START
|
||||
- BTN_MODE
|
||||
- BTN_THUMBL
|
||||
- BTN_THUMBR
|
||||
preset: gamepad
|
||||
- name: right-stick
|
||||
type: physical
|
||||
device_name: VIRPIL Controls 20220407 R-VPC Stick MT-50CM2
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
## 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.
|
|
@ -1,18 +1,13 @@
|
|||
devices:
|
||||
- name: primary
|
||||
type: virtual
|
||||
num_buttons: 74
|
||||
num_axes: 8
|
||||
preset: joystick
|
||||
- name: secondary
|
||||
type: virtual
|
||||
num_buttons: 74
|
||||
num_axes: 3
|
||||
preset: joystick
|
||||
- name: mouse
|
||||
type: virtual
|
||||
num_buttons: 0
|
||||
num_axes: 0
|
||||
rel_axes:
|
||||
- REL_WHEEL
|
||||
preset: mouse
|
||||
- name: right-stick
|
||||
type: physical
|
||||
device_name: VIRPIL Controls 20220407 R-VPC Stick MT-50CM2
|
||||
|
|
|
@ -2,29 +2,42 @@
|
|||
|
||||
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 these parameters:
|
||||
|
||||
* `name` - This is an identifier that your rules will use to refer to the device. It is recommended to avoid spaces or special characters.
|
||||
* `type` - Should be `physical` for an input device, and `virtual` for an output device.
|
||||
* `type` - 'physical' for an input device, 'virtual' for an output device.
|
||||
|
||||
`physical` devices must additionally define these parameters:
|
||||
### Physical Devices
|
||||
|
||||
`physical` devices have these additional parameters:
|
||||
|
||||
* `device_name` - The name of the device as reported by the included `evinfo` command. If your device name ends with a space, use quotation marks (`""`) around the name.
|
||||
* `device_path` - If you have multiple devices that report the same name, you can use `device_path` instead of `device_name`. Setting this will cause the device to be opened directly via the device file.
|
||||
* It is recommended to use the `by-path` symlinks, e.g., `/dev/input/by-path/pci-0000:0d:00.0-usbv2-0:3:1.0-event-joystick`.
|
||||
* Note that this method may be slightly unreliable since these identifiers may change if they are plugged into different USB ports or in the rare case that the USB topology changes (e.g., you add a new USB hub).
|
||||
* On the other hand, this method causes the device to be opened considerably faster, lowering Joyful's startup time substantially. If this is important to you this method may be preferable.
|
||||
* `lock` - If set to 'true', the device will be locked for exclusive access. This means that your game will not see any events from the device, so you'll need to make sure you map every button you want to use. Setting this to 'false' might be useful if you're just mapping a few joystick buttons to keyboard buttons. This value defaults to 'true'.
|
||||
|
||||
`virtual` devices can additionally define these parameters:
|
||||
`device_path` is given higher priority than `device_name`; if both are specified, `device_path` will be used.
|
||||
|
||||
### Virtual Devices
|
||||
|
||||
`virtual` devices have these additional 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.
|
||||
* `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.
|
||||
|
||||
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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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:
|
||||
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/holoplot/go-evdev"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type EventCodeParserTests struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestRunnerEventCodeParserTests(t *testing.T) {
|
||||
suite.Run(t, new(EventCodeParserTests))
|
||||
}
|
||||
|
||||
func parseCodeTestCase(t *EventCodeParserTests, in string, out int, prefix string) {
|
||||
t.Run(fmt.Sprintf("%s: %s", prefix, in), func() {
|
||||
code, err := parseCode(in, prefix)
|
||||
t.Nil(err)
|
||||
t.EqualValues(out, code)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *EventCodeParserTests) TestParseCodeABS() {
|
||||
testCases := []struct {
|
||||
in string
|
||||
out int
|
||||
}{
|
||||
{"ABS_X", evdev.ABS_X},
|
||||
{"ABS_Y", evdev.ABS_Y},
|
||||
{"ABS_Z", evdev.ABS_Z},
|
||||
{"ABS_RX", evdev.ABS_RX},
|
||||
{"ABS_RY", evdev.ABS_RY},
|
||||
{"ABS_RZ", evdev.ABS_RZ},
|
||||
{"ABS_THROTTLE", evdev.ABS_THROTTLE},
|
||||
{"ABS_RUDDER", evdev.ABS_RUDDER},
|
||||
{"x", evdev.ABS_X},
|
||||
{"y", evdev.ABS_Y},
|
||||
{"z", evdev.ABS_Z},
|
||||
{"throttle", evdev.ABS_THROTTLE},
|
||||
{"rudder", evdev.ABS_RUDDER},
|
||||
{"0x0", evdev.ABS_X},
|
||||
{"0x1", evdev.ABS_Y},
|
||||
{"0x2", evdev.ABS_Z},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
parseCodeTestCase(t, testCase.in, testCase.out, "ABS")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *EventCodeParserTests) TestParseCodeREL() {
|
||||
testCases := []struct {
|
||||
in string
|
||||
out int
|
||||
}{
|
||||
{"REL_X", evdev.REL_X},
|
||||
{"REL_Y", evdev.REL_Y},
|
||||
{"REL_Z", evdev.REL_Z},
|
||||
{"REL_RX", evdev.REL_RX},
|
||||
{"REL_RY", evdev.REL_RY},
|
||||
{"REL_RZ", evdev.REL_RZ},
|
||||
{"REL_WHEEL", evdev.REL_WHEEL},
|
||||
{"REL_HWHEEL", evdev.REL_HWHEEL},
|
||||
{"REL_MISC", evdev.REL_MISC},
|
||||
{"x", evdev.REL_X},
|
||||
{"y", evdev.REL_Y},
|
||||
{"wheel", evdev.REL_WHEEL},
|
||||
{"0x0", evdev.REL_X},
|
||||
{"0x1", evdev.REL_Y},
|
||||
{"0x2", evdev.REL_Z},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
parseCodeTestCase(t, testCase.in, testCase.out, "REL")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *EventCodeParserTests) TestParseCodeBTN() {
|
||||
testCases := []struct {
|
||||
in string
|
||||
out int
|
||||
}{
|
||||
{"BTN_TRIGGER", evdev.BTN_TRIGGER},
|
||||
{"trigger", evdev.BTN_TRIGGER},
|
||||
{"0", evdev.BTN_TRIGGER},
|
||||
{"0x120", evdev.BTN_TRIGGER},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
parseCodeTestCase(t, testCase.in, testCase.out, "BTN")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *EventCodeParserTests) TestParseCodeInvalid() {
|
||||
testCases := []struct {
|
||||
in string
|
||||
prefix string
|
||||
}{
|
||||
{"badbutton", "BTN"},
|
||||
{"ABS_X", "BTN"},
|
||||
{"!@#$%^&*(){}-_", "BTN"},
|
||||
{"REL_X", "ABS"},
|
||||
{"ABS_W", "ABS"},
|
||||
{"0", "ABS"},
|
||||
{"0xg", "ABS"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(fmt.Sprintf("%s - '%s'", testCase.prefix, testCase.in), func() {
|
||||
_, err := parseCode(testCase.in, testCase.prefix)
|
||||
t.NotNil(err)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
// The ConfigParser is the main structure you'll interact with when using this package.
|
||||
//
|
||||
// Example usage:
|
||||
// config := &config.ConfigParser{}
|
||||
// config.Parse(<some directory containing YAML files>)
|
||||
// virtualDevices := config.CreateVirtualDevices()
|
||||
// physicalDevices := config.ConnectVirtualDevices()
|
||||
// modes := config.GetModes()
|
||||
// rules := config.BuildRules(physicalDevices, virtualDevices, modes)
|
||||
//
|
||||
// nb: there are methods defined on ConfigParser in other files in this package!
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
type ConfigParser struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// Parse all the config files and store the config data for further use
|
||||
func (parser *ConfigParser) Parse(directory string) error {
|
||||
parser.config = Config{}
|
||||
|
||||
// Find the config files in the directory
|
||||
dirEntries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
err = os.Mkdir(directory, 0755)
|
||||
if err != nil {
|
||||
return errors.New("Failed to create config directory at " + directory)
|
||||
}
|
||||
}
|
||||
|
||||
// Open each yaml file and add its contents to the global config
|
||||
for _, file := range dirEntries {
|
||||
name := file.Name()
|
||||
if file.IsDir() || !(strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")) {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(directory, name)
|
||||
if strings.HasSuffix(filePath, ".yaml") || strings.HasSuffix(filePath, ".yml") {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
logger.LogError(err, "Error while opening config file")
|
||||
continue
|
||||
}
|
||||
newConfig := Config{}
|
||||
err = yaml.Unmarshal(data, &newConfig)
|
||||
logger.LogIfError(err, "Error parsing YAML")
|
||||
parser.config.Rules = append(parser.config.Rules, newConfig.Rules...)
|
||||
parser.config.Devices = append(parser.config.Devices, newConfig.Devices...)
|
||||
parser.config.Modes = append(parser.config.Modes, newConfig.Modes...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(parser.config.Devices) == 0 {
|
||||
return errors.New("Found no devices in configuration. Please add configuration at " + directory)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (parser *ConfigParser) GetModes() []string {
|
||||
if len(parser.config.Modes) == 0 {
|
||||
return []string{"*"}
|
||||
}
|
||||
return parser.config.Modes
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
// CreateVirtualDevices will register any configured devices with type = virtual
|
||||
// using /dev/uinput, and return a map of those devices.
|
||||
//
|
||||
// This function assumes you have already called Parse() on the config directory.
|
||||
//
|
||||
// This function should only be called once, unless you want to create duplicate devices for some reason.
|
||||
func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice {
|
||||
deviceMap := make(map[string]*evdev.InputDevice)
|
||||
|
||||
for _, deviceConfig := range parser.config.Devices {
|
||||
if strings.ToLower(deviceConfig.Type) != DeviceTypeVirtual {
|
||||
continue
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("joyful-%s", deviceConfig.Name)
|
||||
capabilities := map[evdev.EvType][]evdev.EvCode{
|
||||
evdev.EV_KEY: makeButtons(deviceConfig.NumButtons, deviceConfig.Buttons),
|
||||
evdev.EV_ABS: makeAxes(deviceConfig.NumAxes, deviceConfig.Axes),
|
||||
evdev.EV_REL: makeRelativeAxes(deviceConfig.NumRelativeAxes, deviceConfig.RelativeAxes),
|
||||
}
|
||||
|
||||
device, err := evdev.CreateDevice(
|
||||
name,
|
||||
// TODO: who knows what these should actually be
|
||||
evdev.InputID{
|
||||
BusType: 0x03,
|
||||
Vendor: 0x4711,
|
||||
Product: 0x0816,
|
||||
Version: 1,
|
||||
},
|
||||
capabilities,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.LogIfError(err, "Failed to create virtual device")
|
||||
continue
|
||||
}
|
||||
|
||||
deviceMap[deviceConfig.Name] = device
|
||||
logger.Log(fmt.Sprintf(
|
||||
"Created virtual device '%s' with %d buttons, %d axes, and %d relative axes",
|
||||
name,
|
||||
len(capabilities[evdev.EV_KEY]),
|
||||
len(capabilities[evdev.EV_ABS]),
|
||||
len(capabilities[evdev.EV_REL]),
|
||||
))
|
||||
}
|
||||
|
||||
return deviceMap
|
||||
}
|
||||
|
||||
// ConnectPhysicalDevices will create InputDevices corresponding to any registered
|
||||
// devices with type = physical. It will also attempt to acquire exclusive access
|
||||
// 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 should only be called once.
|
||||
func (parser *ConfigParser) ConnectPhysicalDevices(lock bool) map[string]*evdev.InputDevice {
|
||||
deviceMap := make(map[string]*evdev.InputDevice)
|
||||
|
||||
for _, deviceConfig := range parser.config.Devices {
|
||||
if strings.ToLower(deviceConfig.Type) != DeviceTypePhysical {
|
||||
continue
|
||||
}
|
||||
|
||||
device, err := evdev.OpenByName(deviceConfig.DeviceName)
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to open physical device, skipping. Confirm the device name with 'evlist'. Watch out for spaces.")
|
||||
continue
|
||||
}
|
||||
|
||||
if lock {
|
||||
err := device.Grab()
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to grab device for exclusive access")
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log(fmt.Sprintf("Connected to '%s' as '%s'", deviceConfig.DeviceName, deviceConfig.Name))
|
||||
deviceMap[deviceConfig.Name] = device
|
||||
}
|
||||
|
||||
return deviceMap
|
||||
}
|
||||
|
||||
// TODO: these functions have a lot of duplication; we need to figure out how to refactor it cleanly
|
||||
// without losing logging context...
|
||||
func makeButtons(numButtons int, buttonList []string) []evdev.EvCode {
|
||||
if numButtons > 0 && len(buttonList) > 0 {
|
||||
logger.Log("'num_buttons' and 'buttons' both specified, ignoring 'num_buttons'")
|
||||
}
|
||||
|
||||
if numButtons > VirtualDeviceMaxButtons {
|
||||
numButtons = VirtualDeviceMaxButtons
|
||||
logger.Logf("Limiting virtual device buttons to %d", VirtualDeviceMaxButtons)
|
||||
}
|
||||
|
||||
if len(buttonList) > 0 {
|
||||
buttons := make([]evdev.EvCode, 0, len(buttonList))
|
||||
for _, codeStr := range buttonList {
|
||||
code, err := parseCode(codeStr, "BTN")
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create button, skipping")
|
||||
continue
|
||||
}
|
||||
buttons = append(buttons, code)
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
buttons := make([]evdev.EvCode, numButtons)
|
||||
|
||||
for i := 0; i < numButtons; i++ {
|
||||
buttons[i] = ButtonFromIndex[i]
|
||||
}
|
||||
|
||||
return buttons
|
||||
}
|
||||
|
||||
func makeAxes(numAxes int, axisList []string) []evdev.EvCode {
|
||||
if numAxes > 0 && len(axisList) > 0 {
|
||||
logger.Log("'num_axes' and 'axes' both specified, ignoring 'num_axes'")
|
||||
}
|
||||
|
||||
if len(axisList) > 0 {
|
||||
axes := make([]evdev.EvCode, 0, len(axisList))
|
||||
for _, codeStr := range axisList {
|
||||
code, err := parseCode(codeStr, "ABS")
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create axis, skipping")
|
||||
continue
|
||||
}
|
||||
axes = append(axes, code)
|
||||
}
|
||||
return axes
|
||||
}
|
||||
|
||||
if numAxes > 8 {
|
||||
numAxes = 8
|
||||
logger.Log("Limiting virtual device axes to 8")
|
||||
}
|
||||
|
||||
axes := make([]evdev.EvCode, numAxes)
|
||||
for i := 0; i < numAxes; i++ {
|
||||
axes[i] = evdev.EvCode(i)
|
||||
}
|
||||
|
||||
return axes
|
||||
}
|
||||
|
||||
func makeRelativeAxes(numAxes int, axisList []string) []evdev.EvCode {
|
||||
if numAxes > 0 && len(axisList) > 0 {
|
||||
logger.Log("'num_rel_axes' and 'rel_axes' both specified, ignoring 'num_rel_axes'")
|
||||
}
|
||||
|
||||
if len(axisList) > 0 {
|
||||
axes := make([]evdev.EvCode, 0, len(axisList))
|
||||
for _, codeStr := range axisList {
|
||||
code, err := parseCode(codeStr, "REL")
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create axis, skipping")
|
||||
continue
|
||||
}
|
||||
axes = append(axes, code)
|
||||
}
|
||||
return axes
|
||||
}
|
||||
|
||||
if numAxes > 10 {
|
||||
numAxes = 10
|
||||
logger.Log("Limiting virtual device relative axes to 10")
|
||||
}
|
||||
|
||||
axes := make([]evdev.EvCode, numAxes)
|
||||
for i := 0; i < numAxes; i++ {
|
||||
axes[i] = evdev.EvCode(i)
|
||||
}
|
||||
|
||||
return axes
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package config
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
|
||||
type Device interface {
|
||||
AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error)
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
eventCode, err := parseCode(targetConfig.Button, "BTN")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewRuleTargetButton(
|
||||
targetConfig.Device,
|
||||
device,
|
||||
eventCode,
|
||||
targetConfig.Inverted,
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if targetConfig.DeadzoneEnd < targetConfig.DeadzoneStart {
|
||||
return nil, errors.New("deadzone_end must be greater than deadzone_start")
|
||||
}
|
||||
|
||||
eventCode, err := parseCode(targetConfig.Axis, "ABS")
|
||||
if err != nil {
|
||||
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,
|
||||
deadzoneStart,
|
||||
deadzoneEnd,
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
eventCode, err := parseCode(targetConfig.Axis, "REL")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewRuleTargetRelaxis(
|
||||
targetConfig.Device,
|
||||
device,
|
||||
eventCode,
|
||||
targetConfig.Inverted,
|
||||
)
|
||||
}
|
||||
|
||||
func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, allModes []string) (*mappingrules.RuleTargetModeSelect, error) {
|
||||
if ok := validateModes(targetConfig.Modes, allModes); !ok {
|
||||
return nil, errors.New("undefined mode in mode select list")
|
||||
}
|
||||
|
||||
return mappingrules.NewRuleTargetModeSelect(targetConfig.Modes)
|
||||
}
|
||||
|
||||
// hasError exists solely to switch on errors in case statements
|
||||
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) {
|
||||
if start < min {
|
||||
end += min - start
|
||||
start = min
|
||||
}
|
||||
if end > max {
|
||||
start -= end - max
|
||||
end = max
|
||||
}
|
||||
|
||||
return start, end
|
||||
}
|
|
@ -1,244 +0,0 @@
|
|||
package config
|
||||
|
||||
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]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) {
|
||||
suite.Run(t, new(MakeRuleTargetsTests))
|
||||
}
|
||||
|
||||
func (t *MakeRuleTargetsTests) SetupSuite() {
|
||||
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() {
|
||||
t.Run("Standard keycode", func() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
t.config.Button = "74"
|
||||
_, err := makeRuleTargetButton(t.config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
|
||||
t.Run("Un-prefixed keycode", func() {
|
||||
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() {
|
||||
t.config.Button = "foo"
|
||||
_, err := makeRuleTargetButton(t.config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
|
||||
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 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 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 code", func() {
|
||||
t.config.Axis = "foo"
|
||||
_, err := makeRuleTargetAxis(t.config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
|
||||
t.Run("Invalid deadzone", func() {
|
||||
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() {
|
||||
t.Run("Standard keycode", func() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
t.config.Axis = "foo"
|
||||
_, err := makeRuleTargetRelaxis(t.config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
|
||||
t.Run("Incorrect axis type", func() {
|
||||
t.config.Axis = "ABS_X"
|
||||
_, err := makeRuleTargetRelaxis(t.config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
}
|
|
@ -1,230 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
// 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[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(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
|
||||
|
||||
if ok := validateModes(ruleConfig.Modes, modes); !ok {
|
||||
logger.Logf("Skipping rule '%s', mode list specifies undefined mode.", ruleConfig.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
base := mappingrules.NewMappingRuleBase(ruleConfig.Name, ruleConfig.Modes)
|
||||
|
||||
switch strings.ToLower(ruleConfig.Type) {
|
||||
case RuleTypeButton:
|
||||
newRule, err = makeMappingRuleButton(ruleConfig, pDevs, vDevs, base)
|
||||
case RuleTypeButtonCombo:
|
||||
newRule, err = makeMappingRuleCombo(ruleConfig, pDevs, vDevs, base)
|
||||
case RuleTypeLatched:
|
||||
newRule, err = makeMappingRuleLatched(ruleConfig, pDevs, vDevs, base)
|
||||
case RuleTypeAxis:
|
||||
newRule, err = makeMappingRuleAxis(ruleConfig, pDevs, vDevs, base)
|
||||
case RuleTypeAxisCombined:
|
||||
newRule, err = makeMappingRuleAxisCombined(ruleConfig, pDevs, vDevs, base)
|
||||
case RuleTypeAxisToButton:
|
||||
newRule, err = makeMappingRuleAxisToButton(ruleConfig, pDevs, vDevs, base)
|
||||
case RuleTypeAxisToRelaxis:
|
||||
newRule, err = makeMappingRuleAxisToRelaxis(ruleConfig, pDevs, vDevs, base)
|
||||
case RuleTypeModeSelect:
|
||||
newRule, err = makeMappingRuleModeSelect(ruleConfig, pDevs, modes, base)
|
||||
default:
|
||||
err = fmt.Errorf("bad rule type '%s' for rule '%s'", ruleConfig.Type, ruleConfig.Name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.LogErrorf(err, "Failed to build rule '%s'", ruleConfig.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
rules = append(rules, newRule)
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
func makeMappingRuleButton(ruleConfig RuleConfig,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) {
|
||||
|
||||
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleButton(base, input, output), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleCombo(ruleConfig RuleConfig,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) {
|
||||
|
||||
inputs := make([]*mappingrules.RuleTargetButton, 0)
|
||||
for _, inputConfig := range ruleConfig.Inputs {
|
||||
input, err := makeRuleTargetButton(inputConfig, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inputs = append(inputs, input)
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleButtonCombo(base, inputs, output), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleLatched(ruleConfig RuleConfig,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) {
|
||||
|
||||
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleButtonLatched(base, input, output), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleAxis(ruleConfig RuleConfig,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) {
|
||||
|
||||
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleAxis(base, input, output), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleAxisCombined(ruleConfig RuleConfig,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisCombined, error) {
|
||||
|
||||
inputLower, err := makeRuleTargetAxis(ruleConfig.InputLower, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inputUpper, err := makeRuleTargetAxis(ruleConfig.InputUpper, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleAxisCombined(base, inputLower, inputUpper, output), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleAxisToButton(ruleConfig RuleConfig,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) {
|
||||
|
||||
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleAxisToButton(base, input, output, ruleConfig.RepeatRateMin, ruleConfig.RepeatRateMax), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfig,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToRelaxis, error) {
|
||||
|
||||
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetRelaxis(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleAxisToRelaxis(base,
|
||||
input, output,
|
||||
ruleConfig.RepeatRateMin,
|
||||
ruleConfig.RepeatRateMax,
|
||||
ruleConfig.Increment), nil
|
||||
}
|
||||
|
||||
func makeMappingRuleModeSelect(ruleConfig RuleConfig,
|
||||
pDevs map[string]Device,
|
||||
modes []string,
|
||||
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) {
|
||||
|
||||
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := makeRuleTargetModeSelect(ruleConfig.Output, modes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingrules.NewMappingRuleModeSelect(base, input, output), nil
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package config
|
||||
|
||||
import "slices"
|
||||
|
||||
// validateModes checks the provided modes against a larger subset of modes (usually all defined ones)
|
||||
// and returns false if any of the modes are not defined.
|
||||
func validateModes(modes []string, allModes []string) bool {
|
||||
if len(modes) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
if !slices.Contains(allModes, mode) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
// These types comprise the YAML schema for configuring Joyful.
|
||||
// The config files will be combined and then unmarshalled into this
|
||||
//
|
||||
// TODO: currently the types in here aren't especially strong; each one is
|
||||
// decomposed into a different object based on the Type fields. We should implement
|
||||
// some sort of delayed unmarshalling technique, for example see ideas at
|
||||
// https://stackoverflow.com/questions/70635636/unmarshaling-yaml-into-different-struct-based-off-yaml-field
|
||||
// Then we can be more explicit about the interface here.
|
||||
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
Devices []DeviceConfig `yaml:"devices"`
|
||||
Modes []string `yaml:"modes,omitempty"`
|
||||
Rules []RuleConfig `yaml:"rules"`
|
||||
}
|
||||
|
||||
type DeviceConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
DeviceName string `yaml:"device_name,omitempty"`
|
||||
Uuid string `yaml:"uuid,omitempty"`
|
||||
NumButtons int `yaml:"num_buttons,omitempty"`
|
||||
NumAxes int `yaml:"num_axes,omitempty"`
|
||||
NumRelativeAxes int `yaml:"num_rel_axes"`
|
||||
Buttons []string `yaml:"buttons,omitempty"`
|
||||
Axes []string `yaml:"axes,omitempty"`
|
||||
RelativeAxes []string `yaml:"rel_axes,omitempty"`
|
||||
}
|
||||
|
||||
type RuleConfig struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Type string `yaml:"type"`
|
||||
Input RuleTargetConfig `yaml:"input,omitempty"`
|
||||
InputLower RuleTargetConfig `yaml:"input_lower,omitempty"`
|
||||
InputUpper RuleTargetConfig `yaml:"input_upper,omitempty"`
|
||||
Inputs []RuleTargetConfig `yaml:"inputs,omitempty"`
|
||||
Output RuleTargetConfig `yaml:"output"`
|
||||
Modes []string `yaml:"modes,omitempty"`
|
||||
RepeatRateMin int `yaml:"repeat_rate_min,omitempty"`
|
||||
RepeatRateMax int `yaml:"repeat_rate_max,omitempty"`
|
||||
Increment int `yaml:"increment,omitempty"`
|
||||
}
|
||||
|
||||
type RuleTargetConfig struct {
|
||||
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"`
|
||||
}
|
67
internal/configparser/configparser.go
Normal file
67
internal/configparser/configparser.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package configparser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
func ParseConfig(directory string) (*Config, error) {
|
||||
config := new(Config)
|
||||
|
||||
configFiles, err := getConfigFilePaths(directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Open each yaml file and add its contents to the global config
|
||||
for _, filePath := range configFiles {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
logger.LogError(err, "Error while opening config file")
|
||||
continue
|
||||
}
|
||||
|
||||
newConfig := Config{}
|
||||
err = yaml.Unmarshal(data, &newConfig)
|
||||
logger.LogIfError(err, "Error parsing YAML")
|
||||
config.Rules = append(config.Rules, newConfig.Rules...)
|
||||
config.Devices = append(config.Devices, newConfig.Devices...)
|
||||
config.Modes = append(config.Modes, newConfig.Modes...)
|
||||
}
|
||||
|
||||
if len(config.Devices) == 0 {
|
||||
return nil, errors.New("Found no devices in configuration. Please add configuration at " + directory)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func getConfigFilePaths(directory string) ([]string, error) {
|
||||
paths := make([]string, 0)
|
||||
|
||||
dirEntries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
err = os.Mkdir(directory, 0755)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range dirEntries {
|
||||
name := strings.ToLower(file.Name())
|
||||
if file.IsDir() || !(strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")) {
|
||||
continue
|
||||
}
|
||||
|
||||
paths = append(paths, filepath.Join(directory, file.Name()))
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
31
internal/configparser/deviceconfig.go
Normal file
31
internal/configparser/deviceconfig.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
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
|
||||
}
|
35
internal/configparser/deviceconfigphysical.go
Normal file
35
internal/configparser/deviceconfigphysical.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
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
|
||||
}
|
40
internal/configparser/devicetype.go
Normal file
40
internal/configparser/devicetype.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
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
|
||||
}
|
60
internal/configparser/ruleconfig.go
Normal file
60
internal/configparser/ruleconfig.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
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
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
53
internal/configparser/ruletype.go
Normal file
53
internal/configparser/ruletype.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
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"
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
}
|
93
internal/configparser/schema.go
Normal file
93
internal/configparser/schema.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
// These types comprise the YAML schema that doesn't need custom unmarshalling.
|
||||
|
||||
package configparser
|
||||
|
||||
type Config struct {
|
||||
Devices []DeviceConfig
|
||||
Modes []string
|
||||
Rules []RuleConfig
|
||||
}
|
||||
|
||||
// TODO: configure custom unmarshaling so we can overload Buttons, Axes, and RelativeAxes...
|
||||
type DeviceConfigVirtual struct {
|
||||
Name string
|
||||
Preset string
|
||||
NumButtons int `yaml:"num_buttons,omitempty"`
|
||||
NumAxes int `yaml:"num_axes,omitempty"`
|
||||
NumRelativeAxes int `yaml:"num_rel_axes"`
|
||||
Buttons []string
|
||||
Axes []string
|
||||
RelativeAxes []string `yaml:"rel_axes,omitempty"`
|
||||
}
|
||||
|
||||
type RuleConfigButton struct {
|
||||
Input RuleTargetConfigButton
|
||||
Output RuleTargetConfigButton
|
||||
}
|
||||
|
||||
type RuleConfigButtonCombo struct {
|
||||
Inputs []RuleTargetConfigButton
|
||||
Output RuleTargetConfigButton
|
||||
}
|
||||
|
||||
type RuleConfigButtonLatched struct {
|
||||
Input RuleTargetConfigButton
|
||||
Output RuleTargetConfigButton
|
||||
}
|
||||
|
||||
type RuleConfigAxis struct {
|
||||
Input RuleTargetConfigAxis
|
||||
Output RuleTargetConfigAxis
|
||||
}
|
||||
|
||||
type RuleConfigAxisCombined struct {
|
||||
InputLower RuleTargetConfigAxis `yaml:"input_lower,omitempty"`
|
||||
InputUpper RuleTargetConfigAxis `yaml:"input_upper,omitempty"`
|
||||
Output RuleTargetConfigAxis
|
||||
}
|
||||
|
||||
type RuleConfigAxisToButton struct {
|
||||
RepeatRateMin int `yaml:"repeat_rate_min,omitempty"`
|
||||
RepeatRateMax int `yaml:"repeat_rate_max,omitempty"`
|
||||
Input RuleTargetConfigAxis
|
||||
Output RuleTargetConfigButton
|
||||
}
|
||||
|
||||
type RuleConfigAxisToRelaxis struct {
|
||||
RepeatRateMin int `yaml:"repeat_rate_min"`
|
||||
RepeatRateMax int `yaml:"repeat_rate_max"`
|
||||
Increment int
|
||||
Input RuleTargetConfigAxis
|
||||
Output RuleTargetConfigRelaxis
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package config
|
||||
package eventcodes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -8,13 +8,23 @@ import (
|
|||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
func parseCode(code, prefix string) (evdev.EvCode, error) {
|
||||
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) {
|
||||
code = strings.ToUpper(code)
|
||||
|
||||
var codeLookup map[string]evdev.EvCode
|
||||
|
||||
switch prefix {
|
||||
case CodePrefixButton:
|
||||
case CodePrefixButton, CodePrefixKey:
|
||||
codeLookup = evdev.KEYFromString
|
||||
case CodePrefixAxis:
|
||||
codeLookup = evdev.ABSFromString
|
||||
|
@ -60,3 +70,8 @@ func parseCode(code, prefix string) (evdev.EvCode, error) {
|
|||
return eventCode, nil
|
||||
}
|
||||
}
|
||||
|
||||
// hasError exists solely to switch on errors in conditional and case statements
|
||||
func hasError(_ any, err error) bool {
|
||||
return err != nil
|
||||
}
|
142
internal/eventcodes/codes_test.go
Normal file
142
internal/eventcodes/codes_test.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package eventcodes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/holoplot/go-evdev"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type EventCodeParserTests struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestRunnerEventCodeParserTests(t *testing.T) {
|
||||
suite.Run(t, new(EventCodeParserTests))
|
||||
}
|
||||
|
||||
func parseCodeTestCase(t *EventCodeParserTests, in string, out evdev.EvCode, prefix string) {
|
||||
t.Run(fmt.Sprintf("%s: %s", prefix, in), func() {
|
||||
code, err := ParseCode(in, prefix)
|
||||
t.Nil(err)
|
||||
t.EqualValues(out, code)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *EventCodeParserTests) TestParseCodeButton() {
|
||||
testCases := []struct {
|
||||
in string
|
||||
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_Y", evdev.ABS_Y},
|
||||
{"ABS_Z", evdev.ABS_Z},
|
||||
{"ABS_RX", evdev.ABS_RX},
|
||||
{"ABS_RY", evdev.ABS_RY},
|
||||
{"ABS_RZ", evdev.ABS_RZ},
|
||||
{"ABS_THROTTLE", evdev.ABS_THROTTLE},
|
||||
{"ABS_RUDDER", evdev.ABS_RUDDER},
|
||||
{"x", evdev.ABS_X},
|
||||
{"y", evdev.ABS_Y},
|
||||
{"z", evdev.ABS_Z},
|
||||
{"throttle", evdev.ABS_THROTTLE},
|
||||
{"rudder", evdev.ABS_RUDDER},
|
||||
{"0x0", evdev.ABS_X},
|
||||
{"0x1", evdev.ABS_Y},
|
||||
{"0x2", evdev.ABS_Z},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
parseCodeTestCase(t, testCase.in, testCase.out, "ABS")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("REL", func() {
|
||||
testCases := []struct {
|
||||
in string
|
||||
out evdev.EvCode
|
||||
}{
|
||||
{"REL_X", evdev.REL_X},
|
||||
{"REL_Y", evdev.REL_Y},
|
||||
{"REL_Z", evdev.REL_Z},
|
||||
{"REL_RX", evdev.REL_RX},
|
||||
{"REL_RY", evdev.REL_RY},
|
||||
{"REL_RZ", evdev.REL_RZ},
|
||||
{"REL_WHEEL", evdev.REL_WHEEL},
|
||||
{"REL_HWHEEL", evdev.REL_HWHEEL},
|
||||
{"REL_MISC", evdev.REL_MISC},
|
||||
{"x", evdev.REL_X},
|
||||
{"y", evdev.REL_Y},
|
||||
{"wheel", evdev.REL_WHEEL},
|
||||
{"0x0", evdev.REL_X},
|
||||
{"0x1", evdev.REL_Y},
|
||||
{"0x2", evdev.REL_Z},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
parseCodeTestCase(t, testCase.in, testCase.out, "REL")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("BTN", func() {
|
||||
testCases := []struct {
|
||||
in string
|
||||
out evdev.EvCode
|
||||
}{
|
||||
{"BTN_TRIGGER", evdev.BTN_TRIGGER},
|
||||
{"trigger", evdev.BTN_TRIGGER},
|
||||
{"0", evdev.BTN_TRIGGER},
|
||||
{"0x120", evdev.BTN_TRIGGER},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
parseCodeTestCase(t, testCase.in, testCase.out, "BTN")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Invalid", func() {
|
||||
testCases := []struct {
|
||||
in string
|
||||
prefix string
|
||||
}{
|
||||
{"badbutton", "BTN"},
|
||||
{"ABS_X", "BTN"},
|
||||
{"!@#$%^&*(){}-_", "BTN"},
|
||||
{"REL_X", "ABS"},
|
||||
{"ABS_W", "ABS"},
|
||||
{"0", "ABS"},
|
||||
{"0xg", "ABS"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(fmt.Sprintf("%s - '%s'", testCase.prefix, testCase.in), func() {
|
||||
_, err := ParseCode(testCase.in, testCase.prefix)
|
||||
t.NotNil(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,30 +1,16 @@
|
|||
package config
|
||||
package eventcodes
|
||||
|
||||
import (
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
import "github.com/holoplot/go-evdev"
|
||||
|
||||
const (
|
||||
DeviceTypePhysical = "physical"
|
||||
DeviceTypeVirtual = "virtual"
|
||||
|
||||
RuleTypeButton = "button"
|
||||
RuleTypeButtonCombo = "button-combo"
|
||||
RuleTypeLatched = "button-latched"
|
||||
RuleTypeAxis = "axis"
|
||||
RuleTypeAxisCombined = "axis-combined"
|
||||
RuleTypeModeSelect = "mode-select"
|
||||
RuleTypeAxisToButton = "axis-to-button"
|
||||
RuleTypeAxisToRelaxis = "axis-to-relaxis"
|
||||
|
||||
CodePrefixButton = "BTN"
|
||||
CodePrefixKey = "KEY"
|
||||
CodePrefixAxis = "ABS"
|
||||
CodePrefixRelaxis = "REL"
|
||||
|
||||
VirtualDeviceMaxButtons = 74
|
||||
)
|
||||
|
||||
var (
|
||||
// Map joystick buttons to integer indices
|
||||
ButtonFromIndex = []evdev.EvCode{
|
||||
evdev.BTN_TRIGGER,
|
||||
evdev.BTN_THUMB,
|
246
internal/mappingrules/init_rule_targets_test.go
Normal file
246
internal/mappingrules/init_rule_targets_test.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
// TODO: these tests should live with their rule_target_* counterparts
|
||||
|
||||
package mappingrules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type MakeRuleTargetsTests struct {
|
||||
suite.Suite
|
||||
devs map[string]Device
|
||||
deviceMock *DeviceMock
|
||||
}
|
||||
|
||||
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) {
|
||||
suite.Run(t, new(MakeRuleTargetsTests))
|
||||
}
|
||||
|
||||
func (t *MakeRuleTargetsTests) SetupSuite() {
|
||||
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) TestMakeRuleTargetButton() {
|
||||
config := configparser.RuleTargetConfigButton{Device: "test"}
|
||||
|
||||
t.Run("Standard keycode", func() {
|
||||
config.Button = "BTN_TRIGGER"
|
||||
rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.BTN_TRIGGER, rule.Button)
|
||||
})
|
||||
|
||||
t.Run("Hex code", func() {
|
||||
config.Button = "0x2fd"
|
||||
rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.EvCode(0x2fd), rule.Button)
|
||||
})
|
||||
|
||||
t.Run("Index", func() {
|
||||
config.Button = "3"
|
||||
rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.BTN_TOP, rule.Button)
|
||||
})
|
||||
|
||||
t.Run("Index too high", func() {
|
||||
config.Button = "74"
|
||||
_, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
|
||||
t.Run("Un-prefixed keycode", func() {
|
||||
config.Button = "pinkie"
|
||||
rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.BTN_PINKIE, rule.Button)
|
||||
})
|
||||
|
||||
t.Run("Invalid keycode", func() {
|
||||
config.Button = "foo"
|
||||
_, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
|
||||
codeTestCases := []struct {
|
||||
input string
|
||||
output evdev.EvCode
|
||||
}{
|
||||
{"ABS_X", evdev.ABS_X},
|
||||
{"0x01", evdev.ABS_Y},
|
||||
{"x", evdev.ABS_X},
|
||||
}
|
||||
|
||||
for _, tc := range codeTestCases {
|
||||
t.Run(fmt.Sprintf("KeyCode %s", tc.input), func() {
|
||||
config := configparser.RuleTargetConfigAxis{Device: "test"}
|
||||
config.Axis = tc.input
|
||||
rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(tc.output, rule.Axis)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Invalid code", func() {
|
||||
config := configparser.RuleTargetConfigAxis{Device: "test"}
|
||||
config.Axis = "foo"
|
||||
_, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
|
||||
t.Run("Invalid deadzone", func() {
|
||||
config := configparser.RuleTargetConfigAxis{Device: "test"}
|
||||
config.Axis = "x"
|
||||
config.DeadzoneEnd = 100
|
||||
config.DeadzoneStart = 1000
|
||||
_, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
|
||||
relDeadzoneTestCases := []struct {
|
||||
inCenter int32
|
||||
inSize int32
|
||||
outStart int32
|
||||
outEnd int32
|
||||
}{
|
||||
{5000, 1000, 4500, 5500},
|
||||
{0, 500, 0, 500},
|
||||
{10000, 500, 9500, 10000},
|
||||
}
|
||||
|
||||
for _, tc := range relDeadzoneTestCases {
|
||||
t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() {
|
||||
config := configparser.RuleTargetConfigAxis{
|
||||
Device: "test",
|
||||
Axis: "x",
|
||||
DeadzoneCenter: tc.inCenter,
|
||||
DeadzoneSize: tc.inSize,
|
||||
}
|
||||
rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
||||
|
||||
t.Nil(err)
|
||||
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
|
||||
outStart int32
|
||||
outEnd int32
|
||||
}{
|
||||
{5000, 10, 4500, 5500},
|
||||
{0, 10, 0, 1000},
|
||||
{10000, 10, 9000, 10000},
|
||||
}
|
||||
|
||||
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",
|
||||
DeadzoneCenter: tc.inCenter,
|
||||
DeadzoneSizePercent: tc.inSizePercent,
|
||||
}
|
||||
rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
||||
|
||||
t.Nil(err)
|
||||
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() {
|
||||
config := configparser.RuleTargetConfigRelaxis{Device: "test"}
|
||||
|
||||
t.Run("Standard keycode", func() {
|
||||
config.Axis = "REL_WHEEL"
|
||||
rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.REL_WHEEL, rule.Axis)
|
||||
})
|
||||
|
||||
t.Run("Hex keycode", func() {
|
||||
config.Axis = "0x00"
|
||||
rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.REL_X, rule.Axis)
|
||||
})
|
||||
|
||||
t.Run("Un-prefixed keycode", func() {
|
||||
config.Axis = "wheel"
|
||||
rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
||||
t.Nil(err)
|
||||
t.EqualValues(evdev.REL_WHEEL, rule.Axis)
|
||||
})
|
||||
|
||||
t.Run("Invalid keycode", func() {
|
||||
config.Axis = "foo"
|
||||
_, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
|
||||
t.Run("Incorrect axis type", func() {
|
||||
config.Axis = "ABS_X"
|
||||
_, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
||||
t.NotNil(err)
|
||||
})
|
||||
}
|
79
internal/mappingrules/init_rules.go
Normal file
79
internal/mappingrules/init_rules.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package mappingrules
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
func ConvertDeviceMap(inputDevs map[string]*evdev.InputDevice) map[string]Device {
|
||||
// Golang can't inspect the concrete map type to determine interface conformance,
|
||||
// so we handle that here.
|
||||
devices := make(map[string]Device)
|
||||
for name, dev := range inputDevs {
|
||||
devices[name] = dev
|
||||
}
|
||||
return devices
|
||||
}
|
||||
|
||||
// NewRule parses a RuleConfig struct and creates and returns the appropriate rule type.
|
||||
// You can remap a map[string]*evdev.InputDevice to our interface type with ConvertDeviceMap
|
||||
func NewRule(config configparser.RuleConfig, pDevs map[string]Device, vDevs map[string]Device, modes []string) (MappingRule, error) {
|
||||
var newRule MappingRule
|
||||
var err error
|
||||
|
||||
if !validateModes(config.Modes, modes) {
|
||||
return nil, errors.New("mode list specifies undefined mode")
|
||||
}
|
||||
|
||||
base := NewMappingRuleBase(config.Name, config.Modes)
|
||||
|
||||
switch config.Type {
|
||||
case configparser.RuleTypeButton:
|
||||
newRule, err = NewMappingRuleButton(config.Config.(configparser.RuleConfigButton), pDevs, vDevs, base)
|
||||
case configparser.RuleTypeButtonCombo:
|
||||
newRule, err = NewMappingRuleButtonCombo(config.Config.(configparser.RuleConfigButtonCombo), pDevs, vDevs, base)
|
||||
case configparser.RuleTypeButtonLatched:
|
||||
newRule, err = NewMappingRuleButtonLatched(config.Config.(configparser.RuleConfigButtonLatched), pDevs, vDevs, base)
|
||||
case configparser.RuleTypeAxis:
|
||||
newRule, err = NewMappingRuleAxis(config.Config.(configparser.RuleConfigAxis), pDevs, vDevs, base)
|
||||
case configparser.RuleTypeAxisCombined:
|
||||
newRule, err = NewMappingRuleAxisCombined(config.Config.(configparser.RuleConfigAxisCombined), pDevs, vDevs, base)
|
||||
case configparser.RuleTypeAxisToButton:
|
||||
newRule, err = NewMappingRuleAxisToButton(config.Config.(configparser.RuleConfigAxisToButton), pDevs, vDevs, base)
|
||||
case configparser.RuleTypeAxisToRelaxis:
|
||||
newRule, err = NewMappingRuleAxisToRelaxis(config.Config.(configparser.RuleConfigAxisToRelaxis), pDevs, vDevs, base)
|
||||
case configparser.RuleTypeModeSelect:
|
||||
newRule, err = NewMappingRuleModeSelect(config.Config.(configparser.RuleConfigModeSelect), pDevs, modes, base)
|
||||
default:
|
||||
// Shouldn't actually be possible to get here...
|
||||
err = fmt.Errorf("bad rule type '%s' for rule '%s'", config.Type, config.Name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.LogErrorf(err, "Failed to build rule '%s'", config.Name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newRule, nil
|
||||
}
|
||||
|
||||
// validateModes checks the provided modes against a larger subset of modes (usually all defined ones)
|
||||
// and returns false if any of the modes are not defined.
|
||||
func validateModes(modes []string, allModes []string) bool {
|
||||
if len(modes) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
if !slices.Contains(allModes, mode) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
package mappingrules
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
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 MappingRuleAxis struct {
|
||||
|
@ -9,12 +12,26 @@ type MappingRuleAxis struct {
|
|||
Output *RuleTargetAxis
|
||||
}
|
||||
|
||||
func NewMappingRuleAxis(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxis {
|
||||
func NewMappingRuleAxis(ruleConfig configparser.RuleConfigAxis,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleAxis, error) {
|
||||
|
||||
input, err := NewRuleTargetAxisFromConfig(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetAxisFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleAxis{
|
||||
MappingRuleBase: base,
|
||||
Input: input,
|
||||
Output: output,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleAxis) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package mappingrules
|
||||
|
||||
import (
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
@ -12,7 +13,26 @@ type MappingRuleAxisCombined struct {
|
|||
Output *RuleTargetAxis
|
||||
}
|
||||
|
||||
func NewMappingRuleAxisCombined(base MappingRuleBase, inputLower *RuleTargetAxis, inputUpper *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxisCombined {
|
||||
func NewMappingRuleAxisCombined(ruleConfig configparser.RuleConfigAxisCombined,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleAxisCombined, error) {
|
||||
|
||||
inputLower, err := NewRuleTargetAxisFromConfig(ruleConfig.InputLower, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inputUpper, err := NewRuleTargetAxisFromConfig(ruleConfig.InputUpper, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetAxisFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inputLower.OutputMax = 0
|
||||
inputUpper.OutputMin = 0
|
||||
return &MappingRuleAxisCombined{
|
||||
|
@ -20,7 +40,7 @@ func NewMappingRuleAxisCombined(base MappingRuleBase, inputLower *RuleTargetAxis
|
|||
InputLower: inputLower,
|
||||
InputUpper: inputUpper,
|
||||
Output: output,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleAxisCombined) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||
|
|
|
@ -38,7 +38,9 @@ func (t *MappingRuleAxisCombinedTests) SetupTest() {
|
|||
}, nil)
|
||||
|
||||
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, 0, 0)
|
||||
t.inputTargetUpper.OutputMin = 0
|
||||
|
||||
t.outputDevice = &evdev.InputDevice{}
|
||||
t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0)
|
||||
|
@ -57,19 +59,30 @@ func (t *MappingRuleAxisCombinedTests) TearDownSubTest() {
|
|||
t.inputDevice.Reset()
|
||||
}
|
||||
|
||||
// TODO: this test sucks
|
||||
func (t *MappingRuleAxisCombinedTests) TestNewMappingRuleAxisCombined() {
|
||||
t.inputDevice.Stub("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{
|
||||
evdev.ABS_X: {Minimum: 0, Maximum: 10000},
|
||||
evdev.ABS_Y: {Minimum: 0, Maximum: 10000},
|
||||
}, nil)
|
||||
|
||||
rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget)
|
||||
rule := &MappingRuleAxisCombined{
|
||||
MappingRuleBase: t.base,
|
||||
InputLower: t.inputTargetLower,
|
||||
InputUpper: t.inputTargetUpper,
|
||||
Output: t.outputTarget,
|
||||
}
|
||||
t.EqualValues(0, rule.InputLower.OutputMax)
|
||||
t.EqualValues(0, rule.InputUpper.OutputMin)
|
||||
}
|
||||
|
||||
func (t *MappingRuleAxisCombinedTests) TestMatchEvent() {
|
||||
rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget)
|
||||
rule := &MappingRuleAxisCombined{
|
||||
MappingRuleBase: t.base,
|
||||
InputLower: t.inputTargetLower,
|
||||
InputUpper: t.inputTargetUpper,
|
||||
Output: t.outputTarget,
|
||||
}
|
||||
|
||||
t.Run("Lower Input", func() {
|
||||
testCases := []struct{ in, out int32 }{
|
||||
|
|
|
@ -3,6 +3,7 @@ package mappingrules
|
|||
import (
|
||||
"time"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
"github.com/jonboulle/clockwork"
|
||||
)
|
||||
|
@ -23,20 +24,34 @@ type MappingRuleAxisToButton struct {
|
|||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func NewMappingRuleAxisToButton(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetButton, repeatRateMin, repeatRateMax int) *MappingRuleAxisToButton {
|
||||
func NewMappingRuleAxisToButton(ruleConfig configparser.RuleConfigAxisToButton,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleAxisToButton, error) {
|
||||
|
||||
input, err := NewRuleTargetAxisFromConfig(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleAxisToButton{
|
||||
MappingRuleBase: base,
|
||||
Input: input,
|
||||
Output: output,
|
||||
RepeatRateMin: repeatRateMin,
|
||||
RepeatRateMax: repeatRateMax,
|
||||
RepeatRateMin: ruleConfig.RepeatRateMin,
|
||||
RepeatRateMax: ruleConfig.RepeatRateMax,
|
||||
lastEvent: time.Now(),
|
||||
nextEvent: NoNextEvent,
|
||||
repeat: repeatRateMin != 0 && repeatRateMax != 0,
|
||||
repeat: ruleConfig.RepeatRateMin != 0 && ruleConfig.RepeatRateMax != 0,
|
||||
pressed: false,
|
||||
active: false,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleAxisToButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||
|
|
|
@ -19,6 +19,44 @@ type MappingRuleAxisToButtonTests struct {
|
|||
base MappingRuleBase
|
||||
}
|
||||
|
||||
func TestRunnerMappingRuleAxisToButtonTests(t *testing.T) {
|
||||
suite.Run(t, new(MappingRuleAxisToButtonTests))
|
||||
}
|
||||
|
||||
// buildTimerRule creates a MappingRuleAxisToButton with a mocked clock
|
||||
func (t *MappingRuleAxisToButtonTests) buildTimerRule(
|
||||
repeatMin,
|
||||
repeatMax int,
|
||||
nextEvent time.Duration) (*MappingRuleAxisToButton, *clockwork.FakeClock) {
|
||||
|
||||
mockClock := clockwork.NewFakeClock()
|
||||
testRule := t.buildRule(repeatMin, repeatMax)
|
||||
testRule.clock = mockClock
|
||||
testRule.lastEvent = testRule.clock.Now()
|
||||
testRule.nextEvent = nextEvent
|
||||
if nextEvent != NoNextEvent {
|
||||
testRule.active = true
|
||||
}
|
||||
return testRule, mockClock
|
||||
}
|
||||
|
||||
// Todo: don't love this repeated logic...
|
||||
func (t *MappingRuleAxisToButtonTests) buildRule(repeatMin, repeatMax int) *MappingRuleAxisToButton {
|
||||
return &MappingRuleAxisToButton{
|
||||
MappingRuleBase: t.base,
|
||||
Input: t.inputRule,
|
||||
Output: t.outputRule,
|
||||
RepeatRateMin: repeatMin,
|
||||
RepeatRateMax: repeatMax,
|
||||
lastEvent: time.Now(),
|
||||
nextEvent: NoNextEvent,
|
||||
repeat: repeatMin != 0 && repeatMax != 0,
|
||||
pressed: false,
|
||||
active: false,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MappingRuleAxisToButtonTests) SetupTest() {
|
||||
mode := "*"
|
||||
t.mode = &mode
|
||||
|
@ -40,7 +78,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
|
|||
|
||||
// A valid input should set a nextevent
|
||||
t.Run("No Repeat", func() {
|
||||
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 0, 0)
|
||||
testRule := t.buildRule(0, 0)
|
||||
|
||||
t.Run("Valid Input", func() {
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
|
@ -62,7 +100,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
|
|||
})
|
||||
|
||||
t.Run("Repeat", func() {
|
||||
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 750, 250)
|
||||
testRule := t.buildRule(750, 250)
|
||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||
Type: evdev.EV_ABS,
|
||||
Code: evdev.ABS_X,
|
||||
|
@ -90,7 +128,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
t.Run("No Repeat", func() {
|
||||
// Get event if called immediately
|
||||
t.Run("Event is available immediately", func() {
|
||||
testRule, _ := buildTimerRule(t, 0, 0, 0)
|
||||
testRule, _ := t.buildTimerRule(0, 0, 0)
|
||||
|
||||
event := testRule.TimerEvent()
|
||||
|
||||
|
@ -100,7 +138,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
|
||||
// Off event on second call
|
||||
t.Run("Event emits off on second call", func() {
|
||||
testRule, _ := buildTimerRule(t, 0, 0, 0)
|
||||
testRule, _ := t.buildTimerRule(0, 0, 0)
|
||||
|
||||
testRule.TimerEvent()
|
||||
event := testRule.TimerEvent()
|
||||
|
@ -111,7 +149,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
|
||||
// No further event, even if we wait a while
|
||||
t.Run("Additional events are not emitted while still active.", func() {
|
||||
testRule, mockClock := buildTimerRule(t, 0, 0, 0)
|
||||
testRule, mockClock := t.buildTimerRule(0, 0, 0)
|
||||
|
||||
testRule.TimerEvent()
|
||||
testRule.TimerEvent()
|
||||
|
@ -125,13 +163,13 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
|
||||
t.Run("Repeat", func() {
|
||||
t.Run("No event if called immediately", func() {
|
||||
testRule, _ := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||
testRule, _ := t.buildTimerRule(100, 10, 50*time.Millisecond)
|
||||
event := testRule.TimerEvent()
|
||||
t.Nil(event)
|
||||
})
|
||||
|
||||
t.Run("No event after 49ms", func() {
|
||||
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||
testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond)
|
||||
mockClock.Advance(49 * time.Millisecond)
|
||||
|
||||
event := testRule.TimerEvent()
|
||||
|
@ -140,7 +178,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
})
|
||||
|
||||
t.Run("Event after 50ms", func() {
|
||||
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||
testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond)
|
||||
mockClock.Advance(50 * time.Millisecond)
|
||||
|
||||
event := testRule.TimerEvent()
|
||||
|
@ -150,7 +188,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
})
|
||||
|
||||
t.Run("Additional event at 100ms", func() {
|
||||
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||
testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond)
|
||||
|
||||
mockClock.Advance(50 * time.Millisecond)
|
||||
testRule.TimerEvent()
|
||||
|
@ -163,24 +201,3 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunnerMappingRuleAxisToButtonTests(t *testing.T) {
|
||||
suite.Run(t, new(MappingRuleAxisToButtonTests))
|
||||
}
|
||||
|
||||
// buildTimerRule creates a MappingRuleAxisToButton with a mocked clock
|
||||
func buildTimerRule(t *MappingRuleAxisToButtonTests,
|
||||
repeatMin,
|
||||
repeatMax int,
|
||||
nextEvent time.Duration) (*MappingRuleAxisToButton, *clockwork.FakeClock) {
|
||||
|
||||
mockClock := clockwork.NewFakeClock()
|
||||
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, repeatMin, repeatMax)
|
||||
testRule.clock = mockClock
|
||||
testRule.lastEvent = testRule.clock.Now()
|
||||
testRule.nextEvent = nextEvent
|
||||
if nextEvent != NoNextEvent {
|
||||
testRule.active = true
|
||||
}
|
||||
return testRule, mockClock
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package mappingrules
|
|||
import (
|
||||
"time"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
"github.com/jonboulle/clockwork"
|
||||
)
|
||||
|
@ -23,23 +24,32 @@ type MappingRuleAxisToRelaxis struct {
|
|||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func NewMappingRuleAxisToRelaxis(
|
||||
base MappingRuleBase,
|
||||
input *RuleTargetAxis,
|
||||
output *RuleTargetRelaxis,
|
||||
repeatRateMin, repeatRateMax, increment int) *MappingRuleAxisToRelaxis {
|
||||
func NewMappingRuleAxisToRelaxis(ruleConfig configparser.RuleConfigAxisToRelaxis,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleAxisToRelaxis, error) {
|
||||
|
||||
input, err := NewRuleTargetAxisFromConfig(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetRelaxisFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleAxisToRelaxis{
|
||||
MappingRuleBase: base,
|
||||
Input: input,
|
||||
Output: output,
|
||||
RepeatRateMin: repeatRateMin,
|
||||
RepeatRateMax: repeatRateMax,
|
||||
Increment: int32(increment),
|
||||
RepeatRateMin: ruleConfig.RepeatRateMin,
|
||||
RepeatRateMax: ruleConfig.RepeatRateMax,
|
||||
Increment: int32(ruleConfig.Increment),
|
||||
lastEvent: time.Now(),
|
||||
nextEvent: NoNextEvent,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleAxisToRelaxis) MatchEvent(
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package mappingrules
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
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 MappingRuleButton struct {
|
||||
|
@ -9,16 +12,26 @@ type MappingRuleButton struct {
|
|||
Output *RuleTargetButton
|
||||
}
|
||||
|
||||
func NewMappingRuleButton(
|
||||
base MappingRuleBase,
|
||||
input *RuleTargetButton,
|
||||
output *RuleTargetButton) *MappingRuleButton {
|
||||
func NewMappingRuleButton(ruleConfig configparser.RuleConfigButton,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleButton, error) {
|
||||
|
||||
input, err := NewRuleTargetButtonFromConfig(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleButton{
|
||||
MappingRuleBase: base,
|
||||
Input: input,
|
||||
Output: output,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package mappingrules
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
import (
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
// A Combo Mapping Rule can require multiple physical button presses for a single output button
|
||||
type MappingRuleButtonCombo struct {
|
||||
|
@ -10,17 +13,31 @@ type MappingRuleButtonCombo struct {
|
|||
State int
|
||||
}
|
||||
|
||||
func NewMappingRuleButtonCombo(
|
||||
base MappingRuleBase,
|
||||
inputs []*RuleTargetButton,
|
||||
output *RuleTargetButton) *MappingRuleButtonCombo {
|
||||
func NewMappingRuleButtonCombo(ruleConfig configparser.RuleConfigButtonCombo,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleButtonCombo, error) {
|
||||
|
||||
inputs := make([]*RuleTargetButton, 0)
|
||||
for _, inputConfig := range ruleConfig.Inputs {
|
||||
input, err := NewRuleTargetButtonFromConfig(inputConfig, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inputs = append(inputs, input)
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleButtonCombo{
|
||||
MappingRuleBase: base,
|
||||
Inputs: inputs,
|
||||
Output: output,
|
||||
State: 0,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleButtonCombo) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package mappingrules
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
import (
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
type MappingRuleButtonLatched struct {
|
||||
MappingRuleBase
|
||||
|
@ -9,17 +12,27 @@ type MappingRuleButtonLatched struct {
|
|||
State bool
|
||||
}
|
||||
|
||||
func NewMappingRuleButtonLatched(
|
||||
base MappingRuleBase,
|
||||
input *RuleTargetButton,
|
||||
output *RuleTargetButton) *MappingRuleButtonLatched {
|
||||
func NewMappingRuleButtonLatched(ruleConfig configparser.RuleConfigButtonLatched,
|
||||
pDevs map[string]Device,
|
||||
vDevs map[string]Device,
|
||||
base MappingRuleBase) (*MappingRuleButtonLatched, error) {
|
||||
|
||||
input, err := NewRuleTargetButtonFromConfig(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleButtonLatched{
|
||||
MappingRuleBase: base,
|
||||
Input: input,
|
||||
Output: output,
|
||||
State: false,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleButtonLatched) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||
|
|
|
@ -28,7 +28,11 @@ func (t *MappingRuleButtonTests) SetupTest() {
|
|||
func (t *MappingRuleButtonTests) TestMatchEvent() {
|
||||
inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, false)
|
||||
outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false)
|
||||
testRule := NewMappingRuleButton(t.base, inputButton, outputButton)
|
||||
testRule := &MappingRuleButton{
|
||||
MappingRuleBase: t.base,
|
||||
Input: inputButton,
|
||||
Output: outputButton,
|
||||
}
|
||||
|
||||
// A matching input event should produce an output event
|
||||
expected := &evdev.InputEvent{
|
||||
|
@ -58,7 +62,11 @@ func (t *MappingRuleButtonTests) TestMatchEvent() {
|
|||
func (t *MappingRuleButtonTests) TestMatchEventInverted() {
|
||||
inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, true)
|
||||
outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false)
|
||||
testRule := NewMappingRuleButton(t.base, inputButton, outputButton)
|
||||
testRule := &MappingRuleButton{
|
||||
MappingRuleBase: t.base,
|
||||
Input: inputButton,
|
||||
Output: outputButton,
|
||||
}
|
||||
|
||||
// A matching input event should produce an output event
|
||||
expected := &evdev.InputEvent{
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package mappingrules
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
import (
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
type MappingRuleModeSelect struct {
|
||||
MappingRuleBase
|
||||
|
@ -8,17 +11,26 @@ type MappingRuleModeSelect struct {
|
|||
Output *RuleTargetModeSelect
|
||||
}
|
||||
|
||||
func NewMappingRuleModeSelect(
|
||||
base MappingRuleBase,
|
||||
input *RuleTargetButton,
|
||||
output *RuleTargetModeSelect,
|
||||
) *MappingRuleModeSelect {
|
||||
func NewMappingRuleModeSelect(ruleConfig configparser.RuleConfigModeSelect,
|
||||
pDevs map[string]Device,
|
||||
modes []string,
|
||||
base MappingRuleBase) (*MappingRuleModeSelect, error) {
|
||||
|
||||
input, err := NewRuleTargetButtonFromConfig(ruleConfig.Input, pDevs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewRuleTargetModeSelectFromConfig(ruleConfig.Output, modes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MappingRuleModeSelect{
|
||||
MappingRuleBase: base,
|
||||
Input: input,
|
||||
Output: output,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rule *MappingRuleModeSelect) MatchEvent(
|
||||
|
|
|
@ -28,3 +28,16 @@ func Clamp[T Numeric](value, min, max T) T {
|
|||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func clampAndShift(start, end, min, max int32) (int32, int32) {
|
||||
if start < min {
|
||||
end += min - start
|
||||
start = min
|
||||
}
|
||||
if end > max {
|
||||
start -= end - max
|
||||
end = max
|
||||
}
|
||||
|
||||
return start, end
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
|
@ -20,6 +22,77 @@ type RuleTargetAxis struct {
|
|||
deadzoneSize int32
|
||||
}
|
||||
|
||||
func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis, devs map[string]Device) (*RuleTargetAxis, error) {
|
||||
device, ok := devs[targetConfig.Device]
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRuleTargetAxis(
|
||||
targetConfig.Device,
|
||||
device,
|
||||
eventCode,
|
||||
targetConfig.Inverted,
|
||||
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,
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
package mappingrules
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
type RuleTargetButton struct {
|
||||
DeviceName string
|
||||
|
@ -9,6 +15,25 @@ type RuleTargetButton struct {
|
|||
Inverted bool
|
||||
}
|
||||
|
||||
func NewRuleTargetButtonFromConfig(targetConfig configparser.RuleTargetConfigButton, devs map[string]Device) (*RuleTargetButton, error) {
|
||||
device, ok := devs[targetConfig.Device]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||
}
|
||||
|
||||
eventCode, err := eventcodes.ParseCodeButton(targetConfig.Button)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRuleTargetButton(
|
||||
targetConfig.Device,
|
||||
device,
|
||||
eventCode,
|
||||
targetConfig.Inverted,
|
||||
)
|
||||
}
|
||||
|
||||
func NewRuleTargetButton(device_name string, device Device, code evdev.EvCode, inverted bool) (*RuleTargetButton, error) {
|
||||
return &RuleTargetButton{
|
||||
DeviceName: device_name,
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"slices"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
@ -12,6 +13,14 @@ type RuleTargetModeSelect struct {
|
|||
Modes []string
|
||||
}
|
||||
|
||||
func NewRuleTargetModeSelectFromConfig(targetConfig configparser.RuleTargetConfigModeSelect, allModes []string) (*RuleTargetModeSelect, error) {
|
||||
if ok := validateModes(targetConfig.Modes, allModes); !ok {
|
||||
return nil, errors.New("undefined mode in mode select list")
|
||||
}
|
||||
|
||||
return NewRuleTargetModeSelect(targetConfig.Modes)
|
||||
}
|
||||
|
||||
func NewRuleTargetModeSelect(modes []string) (*RuleTargetModeSelect, error) {
|
||||
if len(modes) == 0 {
|
||||
return nil, errors.New("cannot create RuleTargetModeSelect: mode list is empty")
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package mappingrules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
|
@ -8,19 +12,34 @@ type RuleTargetRelaxis struct {
|
|||
DeviceName string
|
||||
Device Device
|
||||
Axis evdev.EvCode
|
||||
Inverted bool
|
||||
}
|
||||
|
||||
func NewRuleTargetRelaxis(device_name string,
|
||||
func NewRuleTargetRelaxisFromConfig(targetConfig configparser.RuleTargetConfigRelaxis, devs map[string]Device) (*RuleTargetRelaxis, error) {
|
||||
device, ok := devs[targetConfig.Device]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||
}
|
||||
|
||||
eventCode, err := eventcodes.ParseCode(targetConfig.Axis, eventcodes.CodePrefixRelaxis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRuleTargetRelaxis(
|
||||
targetConfig.Device,
|
||||
device,
|
||||
eventCode,
|
||||
)
|
||||
}
|
||||
|
||||
func NewRuleTargetRelaxis(deviceName string,
|
||||
device Device,
|
||||
axis evdev.EvCode,
|
||||
inverted bool) (*RuleTargetRelaxis, error) {
|
||||
axis evdev.EvCode) (*RuleTargetRelaxis, error) {
|
||||
|
||||
return &RuleTargetRelaxis{
|
||||
DeviceName: device_name,
|
||||
DeviceName: deviceName,
|
||||
Device: device,
|
||||
Axis: axis,
|
||||
Inverted: inverted,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
// Functions for cleaning up stale virtual devices
|
||||
|
||||
package virtualdevice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
func CleanupStaleVirtualDevices() {
|
||||
devices, err := evdev.ListDevicePaths()
|
||||
if err != nil {
|
||||
fmt.Printf("Couldn't list devices while running cleanup: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, devicePath := range devices {
|
||||
if strings.HasPrefix(devicePath.Name, "joyful-joystick") {
|
||||
device, err := evdev.Open(devicePath.Path)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open existing joyful device at '%s': %s\n", devicePath.Path, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
err = evdev.DestroyDevice(device)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to destroy existing joyful device '%s' at '%s': %s\n", devicePath.Name, devicePath.Path, err.Error())
|
||||
} else {
|
||||
fmt.Printf("Destroyed stale joyful device '%s'\n", devicePath.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,13 +11,7 @@ import (
|
|||
type EventBuffer struct {
|
||||
events []*evdev.InputEvent
|
||||
Device VirtualDevice
|
||||
}
|
||||
|
||||
func NewEventBuffer(device VirtualDevice) *EventBuffer {
|
||||
return &EventBuffer{
|
||||
events: make([]*evdev.InputEvent, 0, 100),
|
||||
Device: device,
|
||||
}
|
||||
Name string
|
||||
}
|
||||
|
||||
func (buffer *EventBuffer) AddEvent(event *evdev.InputEvent) {
|
||||
|
|
|
@ -11,10 +11,11 @@ import (
|
|||
|
||||
type EventBufferTests struct {
|
||||
suite.Suite
|
||||
device *VirtualDeviceMock
|
||||
writeOneCall *mock.Call
|
||||
device *VirtualDeviceMock
|
||||
buffer *EventBuffer
|
||||
}
|
||||
|
||||
// Mocks
|
||||
type VirtualDeviceMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
@ -24,65 +25,65 @@ func (m *VirtualDeviceMock) WriteOne(event *evdev.InputEvent) error {
|
|||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Setup
|
||||
func TestRunnerEventBufferTests(t *testing.T) {
|
||||
suite.Run(t, new(EventBufferTests))
|
||||
}
|
||||
|
||||
func (t *EventBufferTests) SetupTest() {
|
||||
t.device = new(VirtualDeviceMock)
|
||||
}
|
||||
|
||||
func (t *EventBufferTests) SetupSubTest() {
|
||||
t.device = new(VirtualDeviceMock)
|
||||
t.writeOneCall = t.device.On("WriteOne").Return(nil)
|
||||
}
|
||||
|
||||
func (t *EventBufferTests) TearDownSubTest() {
|
||||
t.writeOneCall.Unset()
|
||||
t.buffer = &EventBuffer{Device: t.device}
|
||||
}
|
||||
|
||||
// Tests
|
||||
func (t *EventBufferTests) TestNewEventBuffer() {
|
||||
buffer := NewEventBuffer(t.device)
|
||||
t.Equal(t.device, buffer.Device)
|
||||
t.Len(buffer.events, 0)
|
||||
t.Equal(t.device, t.buffer.Device)
|
||||
t.Len(t.buffer.events, 0)
|
||||
}
|
||||
|
||||
func (t *EventBufferTests) TestEventBufferAddEvent() {
|
||||
buffer := NewEventBuffer(t.device)
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
t.Len(buffer.events, 3)
|
||||
}
|
||||
|
||||
func (t *EventBufferTests) TestEventBufferSendEvents() {
|
||||
t.Run("3 Events", func() {
|
||||
buffer := NewEventBuffer(t.device)
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
errs := buffer.SendEvents()
|
||||
|
||||
t.Len(errs, 0)
|
||||
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 4)
|
||||
})
|
||||
|
||||
t.Run("No Events", func() {
|
||||
buffer := NewEventBuffer(t.device)
|
||||
errs := buffer.SendEvents()
|
||||
|
||||
t.Len(errs, 0)
|
||||
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 0)
|
||||
})
|
||||
|
||||
t.Run("Bad Event", func() {
|
||||
t.writeOneCall.Unset()
|
||||
t.writeOneCall = t.device.On("WriteOne").Return(errors.New("Fail"))
|
||||
|
||||
buffer := NewEventBuffer(t.device)
|
||||
buffer.AddEvent(&evdev.InputEvent{})
|
||||
errs := buffer.SendEvents()
|
||||
t.Len(errs, 2)
|
||||
})
|
||||
|
||||
func (t *EventBufferTests) TestEventBuffer() {
|
||||
|
||||
t.Run("AddEvent", func() {
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
t.Len(t.buffer.events, 3)
|
||||
})
|
||||
|
||||
t.Run("SendEvents", func() {
|
||||
t.Run("3 Events", func() {
|
||||
writeOneCall := t.device.On("WriteOne").Return(nil)
|
||||
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
errs := t.buffer.SendEvents()
|
||||
|
||||
t.Len(errs, 0)
|
||||
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 4)
|
||||
|
||||
writeOneCall.Unset()
|
||||
})
|
||||
|
||||
t.Run("No Events", func() {
|
||||
writeOneCall := t.device.On("WriteOne").Return(nil)
|
||||
|
||||
errs := t.buffer.SendEvents()
|
||||
|
||||
t.Len(errs, 0)
|
||||
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 0)
|
||||
|
||||
writeOneCall.Unset()
|
||||
})
|
||||
|
||||
t.Run("Bad Event", func() {
|
||||
writeOneCall := t.device.On("WriteOne").Return(errors.New("Fail"))
|
||||
|
||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
||||
errs := t.buffer.SendEvents()
|
||||
t.Len(errs, 2)
|
||||
|
||||
writeOneCall.Unset()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
165
internal/virtualdevice/init.go
Normal file
165
internal/virtualdevice/init.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package virtualdevice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||
"github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
// NewEventBuffer takes a virtual device config specification, creates the underlying
|
||||
// evdev.InputDevice, and wraps it in a buffered event emitter.
|
||||
func NewEventBuffer(config configparser.DeviceConfigVirtual) (*EventBuffer, error) {
|
||||
deviceMap := make(map[string]*evdev.InputDevice)
|
||||
|
||||
name := fmt.Sprintf("joyful-%s", config.Name)
|
||||
|
||||
var capabilities map[evdev.EvType][]evdev.EvCode
|
||||
|
||||
// todo: add tests for presets
|
||||
switch config.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(config.NumButtons, config.Buttons),
|
||||
evdev.EV_ABS: makeAxes(config.NumAxes, config.Axes),
|
||||
evdev.EV_REL: makeRelativeAxes(config.NumRelativeAxes, config.RelativeAxes),
|
||||
}
|
||||
}
|
||||
|
||||
device, err := evdev.CreateDevice(
|
||||
name,
|
||||
// TODO: placeholders. Who knows what these should actually be...
|
||||
evdev.InputID{
|
||||
BusType: 0x03,
|
||||
Vendor: 0x4711,
|
||||
Product: 0x0816,
|
||||
Version: 1,
|
||||
},
|
||||
capabilities,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deviceMap[config.Name] = device
|
||||
logger.Log(fmt.Sprintf(
|
||||
"Created virtual device '%s' with %d buttons, %d axes, and %d relative axes",
|
||||
name,
|
||||
len(capabilities[evdev.EV_KEY]),
|
||||
len(capabilities[evdev.EV_ABS]),
|
||||
len(capabilities[evdev.EV_REL]),
|
||||
))
|
||||
|
||||
return &EventBuffer{
|
||||
events: make([]*evdev.InputEvent, 0, 100),
|
||||
Device: device,
|
||||
Name: config.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO: these functions have a lot of duplication; we need to figure out how to refactor it cleanly
|
||||
// without losing logging context...
|
||||
func makeButtons(numButtons int, buttonList []string) []evdev.EvCode {
|
||||
if numButtons > 0 && len(buttonList) > 0 {
|
||||
logger.Log("'num_buttons' and 'buttons' both specified, ignoring 'num_buttons'")
|
||||
}
|
||||
|
||||
if numButtons > VirtualDeviceMaxButtons {
|
||||
numButtons = VirtualDeviceMaxButtons
|
||||
logger.Logf("Limiting virtual device buttons to %d", VirtualDeviceMaxButtons)
|
||||
}
|
||||
|
||||
if len(buttonList) > 0 {
|
||||
buttons := make([]evdev.EvCode, 0, len(buttonList))
|
||||
for _, codeStr := range buttonList {
|
||||
code, err := eventcodes.ParseCode(codeStr, "BTN")
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create button, skipping")
|
||||
continue
|
||||
}
|
||||
buttons = append(buttons, code)
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
buttons := make([]evdev.EvCode, numButtons)
|
||||
|
||||
for i := 0; i < numButtons; i++ {
|
||||
buttons[i] = eventcodes.ButtonFromIndex[i]
|
||||
}
|
||||
|
||||
return buttons
|
||||
}
|
||||
|
||||
func makeAxes(numAxes int, axisList []string) []evdev.EvCode {
|
||||
if numAxes > 0 && len(axisList) > 0 {
|
||||
logger.Log("'num_axes' and 'axes' both specified, ignoring 'num_axes'")
|
||||
}
|
||||
|
||||
if len(axisList) > 0 {
|
||||
axes := make([]evdev.EvCode, 0, len(axisList))
|
||||
for _, codeStr := range axisList {
|
||||
code, err := eventcodes.ParseCode(codeStr, "ABS")
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create axis, skipping")
|
||||
continue
|
||||
}
|
||||
axes = append(axes, code)
|
||||
}
|
||||
return axes
|
||||
}
|
||||
|
||||
if numAxes > 8 {
|
||||
numAxes = 8
|
||||
logger.Log("Limiting virtual device axes to 8")
|
||||
}
|
||||
|
||||
axes := make([]evdev.EvCode, numAxes)
|
||||
for i := 0; i < numAxes; i++ {
|
||||
axes[i] = evdev.EvCode(i)
|
||||
}
|
||||
|
||||
return axes
|
||||
}
|
||||
|
||||
func makeRelativeAxes(numAxes int, axisList []string) []evdev.EvCode {
|
||||
if numAxes > 0 && len(axisList) > 0 {
|
||||
logger.Log("'num_rel_axes' and 'rel_axes' both specified, ignoring 'num_rel_axes'")
|
||||
}
|
||||
|
||||
if len(axisList) > 0 {
|
||||
axes := make([]evdev.EvCode, 0, len(axisList))
|
||||
for _, codeStr := range axisList {
|
||||
code, err := eventcodes.ParseCode(codeStr, "REL")
|
||||
if err != nil {
|
||||
logger.LogError(err, "Failed to create axis, skipping")
|
||||
continue
|
||||
}
|
||||
axes = append(axes, code)
|
||||
}
|
||||
return axes
|
||||
}
|
||||
|
||||
if numAxes > 10 {
|
||||
numAxes = 10
|
||||
logger.Log("Limiting virtual device relative axes to 10")
|
||||
}
|
||||
|
||||
axes := make([]evdev.EvCode, numAxes)
|
||||
for i := 0; i < numAxes; i++ {
|
||||
axes[i] = evdev.EvCode(i)
|
||||
}
|
||||
|
||||
return axes
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package config
|
||||
package virtualdevice
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -7,15 +7,15 @@ import (
|
|||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type DevicesConfigTests struct {
|
||||
type InitTests struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestRunnerDevicesConfig(t *testing.T) {
|
||||
suite.Run(t, new(DevicesConfigTests))
|
||||
func TestRunnerInit(t *testing.T) {
|
||||
suite.Run(t, new(InitTests))
|
||||
}
|
||||
|
||||
func (t *DevicesConfigTests) TestMakeButtons() {
|
||||
func (t *InitTests) TestMakeButtons() {
|
||||
t.Run("Maximum buttons", func() {
|
||||
buttons := makeButtons(VirtualDeviceMaxButtons, []string{})
|
||||
t.Equal(VirtualDeviceMaxButtons, len(buttons))
|
||||
|
@ -44,7 +44,7 @@ func (t *DevicesConfigTests) TestMakeButtons() {
|
|||
})
|
||||
}
|
||||
|
||||
func (t *DevicesConfigTests) TestMakeAxes() {
|
||||
func (t *InitTests) TestMakeAxes() {
|
||||
t.Run("8 axes", func() {
|
||||
axes := makeAxes(8, []string{})
|
||||
t.Equal(8, len(axes))
|
||||
|
@ -81,7 +81,7 @@ func (t *DevicesConfigTests) TestMakeAxes() {
|
|||
})
|
||||
}
|
||||
|
||||
func (t *DevicesConfigTests) TestMakeRelativeAxes() {
|
||||
func (t *InitTests) TestMakeRelativeAxes() {
|
||||
t.Run("10 axes", func() {
|
||||
axes := makeRelativeAxes(10, []string{})
|
||||
t.Equal(10, len(axes))
|
290
internal/virtualdevice/variables.go
Normal file
290
internal/virtualdevice/variables.go
Normal file
|
@ -0,0 +1,290 @@
|
|||
package virtualdevice
|
||||
|
||||
import "github.com/holoplot/go-evdev"
|
||||
|
||||
const (
|
||||
DevicePresetKeyboard = "keyboard"
|
||||
DevicePresetGamepad = "gamepad"
|
||||
DevicePresetJoystick = "joystick"
|
||||
DevicePresetMouse = "mouse"
|
||||
|
||||
VirtualDeviceMaxButtons = 74
|
||||
)
|
||||
|
||||
// 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,
|
||||
},
|
||||
}
|
||||
)
|
16
readme.md
16
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.
|
||||
* 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.
|
||||
* Define multiple modes with per-mode behavior.
|
||||
* Text-to-speech engine that announces the current mode when it changes.
|
||||
|
@ -26,11 +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
|
||||
* Output keyboard button presses
|
||||
* Hat support
|
||||
* HIDRAW support for more button options.
|
||||
* Sensitivity Curves.
|
||||
* Packaged builds for Arch and possibly other distributions.
|
||||
* Sensitivity Curves?
|
||||
* Packaged builds non-Arch distributions.
|
||||
|
||||
## Configure
|
||||
|
||||
|
@ -48,6 +48,8 @@ If you are on Arch or an Arch-based distro, you can get the latest Joyful releas
|
|||
yay -S joyful
|
||||
```
|
||||
|
||||
You may also need to add the user(s) who will be running joyful to the `input` group.
|
||||
|
||||
### Manual Install
|
||||
|
||||
To build joyful manually, first use your distribution's package manager to install the following dependencies:
|
||||
|
@ -87,10 +89,10 @@ Pressing `<enter>` in the running terminal window will reload the `rules` sectio
|
|||
|
||||
## 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
|
||||
|
||||
Send patches and questions to [annabunches@gmail.com](mailto:annabunches@gmail.com). Make sure the subject of your email starts with `[Joyful]`.
|
||||
|
||||
If enough people show an interest in contributing, I'll consider mirroring the repository on Github.
|
||||
Issues and pull requests should be made on the [Codeberg mirror](https://codeberg.org/annabunches/joyful).
|
Loading…
Add table
Add a link
Reference in a new issue