Move initialization code closer to the appropriate structs. (#17)

Reviewed-on: #17
Co-authored-by: Anna Rose Wiggins <annabunches@gmail.com>
Co-committed-by: Anna Rose Wiggins <annabunches@gmail.com>
This commit is contained in:
Anna Rose Wiggins 2025-08-12 00:57:11 +00:00 committed by Anna Rose Wiggins
parent d9babf5dc0
commit 8d2b15a7c8
40 changed files with 1087 additions and 1109 deletions

View file

@ -5,7 +5,8 @@ import (
"slices" "slices"
// TODO: using config here feels like bad coupling... ButtonFromIndex might need a refactor / move // 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" "git.annabunches.net/annabunches/joyful/internal/logger"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
@ -20,7 +21,7 @@ func isJoystickLike(device *evdev.InputDevice) bool {
if slices.Contains(types, evdev.EV_KEY) { if slices.Contains(types, evdev.EV_KEY) {
buttons := device.CapableEvents(evdev.EV_KEY) buttons := device.CapableEvents(evdev.EV_KEY)
for _, code := range config.ButtonFromIndex { for _, code := range eventcodes.ButtonFromIndex {
if slices.Contains(buttons, code) { if slices.Contains(buttons, code) {
return true return true
} }

147
cmd/joyful/config.go Normal file
View file

@ -0,0 +1,147 @@
package main
import (
"context"
"strings"
"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 strings.ToLower(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 strings.ToLower(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
}

View file

@ -1,19 +1,15 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"sync"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
flag "github.com/spf13/pflag" 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/logger"
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
"git.annabunches.net/annabunches/joyful/internal/virtualdevice"
) )
func getConfigDir(dir string) string { func getConfigDir(dir string) string {
@ -21,39 +17,6 @@ func getConfigDir(dir string) string {
return os.ExpandEnv(configDir) 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]*evdev.InputDevice,
map[string]*virtualdevice.EventBuffer,
map[*evdev.InputDevice]*virtualdevice.EventBuffer) {
vDevices := config.InitVirtualDevices()
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 vDevices, vBuffersByName, vBuffersByDevice
}
func initPhysicalDevices(config *config.ConfigParser) map[string]*evdev.InputDevice {
pDeviceMap := config.InitPhysicalDevices()
if len(pDeviceMap) == 0 {
logger.Log("Warning: no physical devices found in configuration. No rules will work.")
}
return pDeviceMap
}
func main() { func main() {
// parse command-line // parse command-line
var configFlag string var configFlag string
@ -64,7 +27,8 @@ func main() {
// parse configs // parse configs
configDir := getConfigDir(configFlag) configDir := getConfigDir(configFlag)
config := readConfig(configDir) config, err := configparser.ParseConfig(configDir)
logger.FatalIfError(err, "Failed to parse configuration")
// initialize TTS // initialize TTS
tts, err := newTTS(ttsOps) tts, err := newTTS(ttsOps)
@ -76,20 +40,26 @@ func main() {
// Initialize physical devices // Initialize physical devices
pDevices := initPhysicalDevices(config) pDevices := initPhysicalDevices(config)
// Load the rules // initialize the mode variables
rules, eventChannel, cancel, wg := loadRules(config, pDevices, vDevicesByName) var mode string
modes := config.Modes
if len(modes) == 0 {
mode = "*"
} else {
mode = config.Modes[0]
}
// initialize the mode variable // Load the rules
mode := config.GetModes()[0] rules, eventChannel, cancel, wg := loadRules(config, pDevices, vDevicesByName, modes)
// initialize TTS phrases for modes // initialize TTS phrases for modes
for _, m := range config.GetModes() { for _, m := range modes {
tts.AddMessage(m) tts.AddMessage(m)
logger.LogDebugf("Added TTS message '%s'", m) logger.LogDebugf("Added TTS message '%s'", m)
} }
fmt.Println("Joyful Running! Press Ctrl+C to quit. Press Enter to reload rules.") 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) logger.Logf("Initial mode set to '%s'", mode)
} }
@ -127,13 +97,18 @@ func main() {
case ChannelEventReload: case ChannelEventReload:
// stop existing channels // 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.") fmt.Println("Reloading rules.")
cancel() cancel()
fmt.Println("Waiting for existing listeners to exit. Provide input from each of your devices.") fmt.Println("Waiting for existing listeners to exit. Provide input from each of your devices.")
wg.Wait() wg.Wait()
fmt.Println("Listeners exited. Parsing config.") fmt.Println("Listeners exited. Loading new rules.")
config := readConfig(configDir) // reload the config rules, eventChannel, cancel, wg = loadRules(config, pDevices, vDevicesByName, modes)
rules, eventChannel, cancel, wg = loadRules(config, pDevices, vDevicesByName)
fmt.Println("Config re-loaded. Only rule changes applied. Device and Mode changes require restart.") fmt.Println("Config re-loaded. Only rule changes applied. Device and Mode changes require restart.")
} }
@ -142,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.InitRules(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
}

View file

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

View file

@ -1,221 +0,0 @@
package config
import (
"fmt"
"strings"
"git.annabunches.net/annabunches/joyful/internal/logger"
"github.com/holoplot/go-evdev"
)
// InitVirtualDevices will register any configured devices with type = virtual
// using /dev/uinput, and return a map of those devices.
//
// This function assumes Parse() has been called.
//
// This function should only be called once, unless we want to create duplicate devices for some reason.
func (parser *ConfigParser) InitVirtualDevices() map[string]*evdev.InputDevice {
deviceMap := make(map[string]*evdev.InputDevice)
for _, deviceConfig := range parser.config.Devices {
if strings.ToLower(deviceConfig.Type) != DeviceTypeVirtual {
continue
}
deviceConfig := deviceConfig.Config.(DeviceConfigVirtual)
name := fmt.Sprintf("joyful-%s", deviceConfig.Name)
var capabilities map[evdev.EvType][]evdev.EvCode
// todo: add tests for presets
switch deviceConfig.Preset {
case DevicePresetGamepad:
capabilities = CapabilitiesPresetGamepad
case DevicePresetKeyboard:
capabilities = CapabilitiesPresetKeyboard
case DevicePresetJoystick:
capabilities = CapabilitiesPresetJoystick
case DevicePresetMouse:
capabilities = CapabilitiesPresetMouse
default:
capabilities = map[evdev.EvType][]evdev.EvCode{
evdev.EV_KEY: makeButtons(deviceConfig.NumButtons, deviceConfig.Buttons),
evdev.EV_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
}
// InitPhysicalDevices will create InputDevices corresponding to any registered
// devices with type = physical.
//
// This function assumes Parse() has been called.
//
// This function should only be called once.
func (parser *ConfigParser) InitPhysicalDevices() map[string]*evdev.InputDevice {
deviceMap := make(map[string]*evdev.InputDevice)
for _, deviceConfig := range parser.config.Devices {
if strings.ToLower(deviceConfig.Type) != DeviceTypePhysical {
continue
}
deviceConfig := deviceConfig.Config.(DeviceConfigPhysical)
var infoName string
var device *evdev.InputDevice
var err error
if deviceConfig.DevicePath != "" {
infoName = deviceConfig.DevicePath
device, err = evdev.Open(deviceConfig.DevicePath)
} else {
infoName = deviceConfig.DeviceName
device, err = evdev.OpenByName(deviceConfig.DeviceName)
}
if err != nil {
logger.LogError(err, "Failed to open physical device, skipping. Confirm the device name or path with 'evinfo'")
continue
}
if deviceConfig.Lock {
logger.LogDebugf("Locking device '%s'", infoName)
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'", infoName, 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
}

View file

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

View file

@ -1,145 +0,0 @@
package config
import (
"errors"
"fmt"
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
"github.com/holoplot/go-evdev"
)
func makeRuleTargetButton(targetConfig RuleTargetConfigButton, 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 := parseCodeButton(targetConfig.Button)
if err != nil {
return nil, err
}
return mappingrules.NewRuleTargetButton(
targetConfig.Device,
device,
eventCode,
targetConfig.Inverted,
)
}
func makeRuleTargetAxis(targetConfig RuleTargetConfigAxis, 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, CodePrefixAxis)
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 RuleTargetConfigRelaxis, 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, CodePrefixRelaxis)
if err != nil {
return nil, err
}
return mappingrules.NewRuleTargetRelaxis(
targetConfig.Device,
device,
eventCode,
)
}
func makeRuleTargetModeSelect(targetConfig RuleTargetConfigModeSelect, 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 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 = 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
}

View file

@ -1,237 +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) InitRules(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.Config.(RuleConfigButton), pDevs, vDevs, base)
case RuleTypeButtonCombo:
newRule, err = makeMappingRuleCombo(ruleConfig.Config.(RuleConfigButtonCombo), pDevs, vDevs, base)
case RuleTypeButtonLatched:
newRule, err = makeMappingRuleLatched(ruleConfig.Config.(RuleConfigButtonLatched), pDevs, vDevs, base)
case RuleTypeAxis:
newRule, err = makeMappingRuleAxis(ruleConfig.Config.(RuleConfigAxis), pDevs, vDevs, base)
case RuleTypeAxisCombined:
newRule, err = makeMappingRuleAxisCombined(ruleConfig.Config.(RuleConfigAxisCombined), pDevs, vDevs, base)
case RuleTypeAxisToButton:
newRule, err = makeMappingRuleAxisToButton(ruleConfig.Config.(RuleConfigAxisToButton), pDevs, vDevs, base)
case RuleTypeAxisToRelaxis:
newRule, err = makeMappingRuleAxisToRelaxis(ruleConfig.Config.(RuleConfigAxisToRelaxis), pDevs, vDevs, base)
case RuleTypeModeSelect:
newRule, err = makeMappingRuleModeSelect(ruleConfig.Config.(RuleConfigModeSelect), 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
}
// TODO: how much of these functions could we fold into the unmarshaling logic itself? The main problem
// is that we don't have access to the device maps in those functions... could we set device names
// as stand-ins and do a post-processing pass that *just* handles device linking and possibly mode
// checking?
//
// In other words - can we unmarshal the config directly into our target structs and remove most of
// this library?
func makeMappingRuleButton(ruleConfig RuleConfigButton,
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 RuleConfigButtonCombo,
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 RuleConfigButtonLatched,
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 RuleConfigAxis,
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 RuleConfigAxisCombined,
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 RuleConfigAxisToButton,
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 RuleConfigAxisToRelaxis,
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 RuleConfigModeSelect,
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
}

View file

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

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

View file

@ -1,7 +1,7 @@
// These types comprise the YAML schema for configuring Joyful. // These types comprise the YAML schema for configuring Joyful.
// The config files will be combined and then unmarshalled into this // The config files will be combined and then unmarshalled into this
package config package configparser
import ( import (
"fmt" "fmt"

View file

@ -0,0 +1,15 @@
package configparser
const (
DeviceTypePhysical = "physical"
DeviceTypeVirtual = "virtual"
RuleTypeButton = "button"
RuleTypeButtonCombo = "button-combo"
RuleTypeButtonLatched = "button-latched"
RuleTypeAxis = "axis"
RuleTypeAxisCombined = "axis-combined"
RuleTypeAxisToButton = "axis-to-button"
RuleTypeAxisToRelaxis = "axis-to-relaxis"
RuleTypeModeSelect = "mode-select"
)

View file

@ -1,4 +1,4 @@
package config package eventcodes
import ( import (
"fmt" "fmt"
@ -8,17 +8,17 @@ import (
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
func parseCodeButton(code string) (evdev.EvCode, error) { func ParseCodeButton(code string) (evdev.EvCode, error) {
prefix := CodePrefixButton prefix := CodePrefixButton
if strings.HasPrefix(code, CodePrefixKey+"_") { if strings.HasPrefix(code, CodePrefixKey+"_") {
prefix = CodePrefixKey prefix = CodePrefixKey
} }
return parseCode(code, prefix) return ParseCode(code, prefix)
} }
func parseCode(code, prefix string) (evdev.EvCode, error) { func ParseCode(code, prefix string) (evdev.EvCode, error) {
code = strings.ToUpper(code) code = strings.ToUpper(code)
var codeLookup map[string]evdev.EvCode var codeLookup map[string]evdev.EvCode
@ -70,3 +70,8 @@ func parseCode(code, prefix string) (evdev.EvCode, error) {
return eventCode, nil return eventCode, nil
} }
} }
// hasError exists solely to switch on errors in conditional and case statements
func hasError(_ any, err error) bool {
return err != nil
}

View file

@ -1,4 +1,4 @@
package config package eventcodes
import ( import (
"fmt" "fmt"
@ -18,7 +18,7 @@ func TestRunnerEventCodeParserTests(t *testing.T) {
func parseCodeTestCase(t *EventCodeParserTests, in string, out evdev.EvCode, prefix string) { func parseCodeTestCase(t *EventCodeParserTests, in string, out evdev.EvCode, prefix string) {
t.Run(fmt.Sprintf("%s: %s", prefix, in), func() { t.Run(fmt.Sprintf("%s: %s", prefix, in), func() {
code, err := parseCode(in, prefix) code, err := ParseCode(in, prefix)
t.Nil(err) t.Nil(err)
t.EqualValues(out, code) t.EqualValues(out, code)
}) })
@ -38,7 +38,7 @@ func (t *EventCodeParserTests) TestParseCodeButton() {
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(testCase.in, func() { t.Run(testCase.in, func() {
code, err := parseCodeButton(testCase.in) code, err := ParseCodeButton(testCase.in)
t.Nil(err) t.Nil(err)
t.EqualValues(code, testCase.out) t.EqualValues(code, testCase.out)
}) })
@ -134,7 +134,7 @@ func (t *EventCodeParserTests) TestParseCode() {
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(fmt.Sprintf("%s - '%s'", testCase.prefix, testCase.in), func() { t.Run(fmt.Sprintf("%s - '%s'", testCase.prefix, testCase.in), func() {
_, err := parseCode(testCase.in, testCase.prefix) _, err := ParseCode(testCase.in, testCase.prefix)
t.NotNil(err) t.NotNil(err)
}) })
} }

View file

@ -0,0 +1,90 @@
package eventcodes
import "github.com/holoplot/go-evdev"
const (
CodePrefixButton = "BTN"
CodePrefixKey = "KEY"
CodePrefixAxis = "ABS"
CodePrefixRelaxis = "REL"
)
var (
// Map joystick buttons to integer indices
ButtonFromIndex = []evdev.EvCode{
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),
}
)

View file

@ -1,9 +1,12 @@
package config // TODO: these tests should live with their rule_target_* counterparts
package mappingrules
import ( import (
"fmt" "fmt"
"testing" "testing"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -48,45 +51,45 @@ func (t *MakeRuleTargetsTests) SetupSuite() {
} }
func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() { func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() {
config := RuleTargetConfigButton{Device: "test"} config := configparser.RuleTargetConfigButton{Device: "test"}
t.Run("Standard keycode", func() { t.Run("Standard keycode", func() {
config.Button = "BTN_TRIGGER" config.Button = "BTN_TRIGGER"
rule, err := makeRuleTargetButton(config, t.devs) rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.BTN_TRIGGER, rule.Button) t.EqualValues(evdev.BTN_TRIGGER, rule.Button)
}) })
t.Run("Hex code", func() { t.Run("Hex code", func() {
config.Button = "0x2fd" config.Button = "0x2fd"
rule, err := makeRuleTargetButton(config, t.devs) rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.EvCode(0x2fd), rule.Button) t.EqualValues(evdev.EvCode(0x2fd), rule.Button)
}) })
t.Run("Index", func() { t.Run("Index", func() {
config.Button = "3" config.Button = "3"
rule, err := makeRuleTargetButton(config, t.devs) rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.BTN_TOP, rule.Button) t.EqualValues(evdev.BTN_TOP, rule.Button)
}) })
t.Run("Index too high", func() { t.Run("Index too high", func() {
config.Button = "74" config.Button = "74"
_, err := makeRuleTargetButton(config, t.devs) _, err := NewRuleTargetButtonFromConfig(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
t.Run("Un-prefixed keycode", func() { t.Run("Un-prefixed keycode", func() {
config.Button = "pinkie" config.Button = "pinkie"
rule, err := makeRuleTargetButton(config, t.devs) rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.BTN_PINKIE, rule.Button) t.EqualValues(evdev.BTN_PINKIE, rule.Button)
}) })
t.Run("Invalid keycode", func() { t.Run("Invalid keycode", func() {
config.Button = "foo" config.Button = "foo"
_, err := makeRuleTargetButton(config, t.devs) _, err := NewRuleTargetButtonFromConfig(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
} }
@ -103,9 +106,9 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
for _, tc := range codeTestCases { for _, tc := range codeTestCases {
t.Run(fmt.Sprintf("KeyCode %s", tc.input), func() { t.Run(fmt.Sprintf("KeyCode %s", tc.input), func() {
config := RuleTargetConfigAxis{Device: "test"} config := configparser.RuleTargetConfigAxis{Device: "test"}
config.Axis = tc.input config.Axis = tc.input
rule, err := makeRuleTargetAxis(config, t.devs) rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(tc.output, rule.Axis) t.EqualValues(tc.output, rule.Axis)
@ -113,18 +116,18 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
} }
t.Run("Invalid code", func() { t.Run("Invalid code", func() {
config := RuleTargetConfigAxis{Device: "test"} config := configparser.RuleTargetConfigAxis{Device: "test"}
config.Axis = "foo" config.Axis = "foo"
_, err := makeRuleTargetAxis(config, t.devs) _, err := NewRuleTargetAxisFromConfig(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
t.Run("Invalid deadzone", func() { t.Run("Invalid deadzone", func() {
config := RuleTargetConfigAxis{Device: "test"} config := configparser.RuleTargetConfigAxis{Device: "test"}
config.Axis = "x" config.Axis = "x"
config.DeadzoneEnd = 100 config.DeadzoneEnd = 100
config.DeadzoneStart = 1000 config.DeadzoneStart = 1000
_, err := makeRuleTargetAxis(config, t.devs) _, err := NewRuleTargetAxisFromConfig(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
@ -141,13 +144,13 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
for _, tc := range relDeadzoneTestCases { for _, tc := range relDeadzoneTestCases {
t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() { t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() {
config := RuleTargetConfigAxis{ config := configparser.RuleTargetConfigAxis{
Device: "test", Device: "test",
Axis: "x", Axis: "x",
DeadzoneCenter: tc.inCenter, DeadzoneCenter: tc.inCenter,
DeadzoneSize: tc.inSize, DeadzoneSize: tc.inSize,
} }
rule, err := makeRuleTargetAxis(config, t.devs) rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
t.Nil(err) t.Nil(err)
t.Equal(tc.outStart, rule.DeadzoneStart) t.Equal(tc.outStart, rule.DeadzoneStart)
@ -156,13 +159,13 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
} }
t.Run("Deadzone center/size invalid center", func() { t.Run("Deadzone center/size invalid center", func() {
config := RuleTargetConfigAxis{ config := configparser.RuleTargetConfigAxis{
Device: "test", Device: "test",
Axis: "x", Axis: "x",
DeadzoneCenter: 20000, DeadzoneCenter: 20000,
DeadzoneSize: 500, DeadzoneSize: 500,
} }
_, err := makeRuleTargetAxis(config, t.devs) _, err := NewRuleTargetAxisFromConfig(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
@ -179,13 +182,13 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
for _, tc := range relDeadzonePercentTestCases { for _, tc := range relDeadzonePercentTestCases {
t.Run(fmt.Sprintf("Relative percent deadzone %d +- %d%%", tc.inCenter, tc.inSizePercent), func() { t.Run(fmt.Sprintf("Relative percent deadzone %d +- %d%%", tc.inCenter, tc.inSizePercent), func() {
config := RuleTargetConfigAxis{ config := configparser.RuleTargetConfigAxis{
Device: "test", Device: "test",
Axis: "x", Axis: "x",
DeadzoneCenter: tc.inCenter, DeadzoneCenter: tc.inCenter,
DeadzoneSizePercent: tc.inSizePercent, DeadzoneSizePercent: tc.inSizePercent,
} }
rule, err := makeRuleTargetAxis(config, t.devs) rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
t.Nil(err) t.Nil(err)
t.Equal(tc.outStart, rule.DeadzoneStart) t.Equal(tc.outStart, rule.DeadzoneStart)
@ -194,50 +197,50 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
} }
t.Run("Deadzone center/percent invalid center", func() { t.Run("Deadzone center/percent invalid center", func() {
config := RuleTargetConfigAxis{ config := configparser.RuleTargetConfigAxis{
Device: "test", Device: "test",
Axis: "x", Axis: "x",
DeadzoneCenter: 20000, DeadzoneCenter: 20000,
DeadzoneSizePercent: 10, DeadzoneSizePercent: 10,
} }
_, err := makeRuleTargetAxis(config, t.devs) _, err := NewRuleTargetAxisFromConfig(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
} }
func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() { func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() {
config := RuleTargetConfigRelaxis{Device: "test"} config := configparser.RuleTargetConfigRelaxis{Device: "test"}
t.Run("Standard keycode", func() { t.Run("Standard keycode", func() {
config.Axis = "REL_WHEEL" config.Axis = "REL_WHEEL"
rule, err := makeRuleTargetRelaxis(config, t.devs) rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.REL_WHEEL, rule.Axis) t.EqualValues(evdev.REL_WHEEL, rule.Axis)
}) })
t.Run("Hex keycode", func() { t.Run("Hex keycode", func() {
config.Axis = "0x00" config.Axis = "0x00"
rule, err := makeRuleTargetRelaxis(config, t.devs) rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.REL_X, rule.Axis) t.EqualValues(evdev.REL_X, rule.Axis)
}) })
t.Run("Un-prefixed keycode", func() { t.Run("Un-prefixed keycode", func() {
config.Axis = "wheel" config.Axis = "wheel"
rule, err := makeRuleTargetRelaxis(config, t.devs) rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.REL_WHEEL, rule.Axis) t.EqualValues(evdev.REL_WHEEL, rule.Axis)
}) })
t.Run("Invalid keycode", func() { t.Run("Invalid keycode", func() {
config.Axis = "foo" config.Axis = "foo"
_, err := makeRuleTargetRelaxis(config, t.devs) _, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
t.Run("Incorrect axis type", func() { t.Run("Incorrect axis type", func() {
config.Axis = "ABS_X" config.Axis = "ABS_X"
_, err := makeRuleTargetRelaxis(config, t.devs) _, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
} }

View file

@ -0,0 +1,79 @@
package mappingrules
import (
"errors"
"fmt"
"slices"
"strings"
"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 strings.ToLower(config.Type) {
case RuleTypeButton:
newRule, err = NewMappingRuleButton(config.Config.(configparser.RuleConfigButton), pDevs, vDevs, base)
case RuleTypeButtonCombo:
newRule, err = NewMappingRuleButtonCombo(config.Config.(configparser.RuleConfigButtonCombo), pDevs, vDevs, base)
case RuleTypeButtonLatched:
newRule, err = NewMappingRuleButtonLatched(config.Config.(configparser.RuleConfigButtonLatched), pDevs, vDevs, base)
case RuleTypeAxis:
newRule, err = NewMappingRuleAxis(config.Config.(configparser.RuleConfigAxis), pDevs, vDevs, base)
case RuleTypeAxisCombined:
newRule, err = NewMappingRuleAxisCombined(config.Config.(configparser.RuleConfigAxisCombined), pDevs, vDevs, base)
case RuleTypeAxisToButton:
newRule, err = NewMappingRuleAxisToButton(config.Config.(configparser.RuleConfigAxisToButton), pDevs, vDevs, base)
case RuleTypeAxisToRelaxis:
newRule, err = NewMappingRuleAxisToRelaxis(config.Config.(configparser.RuleConfigAxisToRelaxis), pDevs, vDevs, base)
case RuleTypeModeSelect:
newRule, err = NewMappingRuleModeSelect(config.Config.(configparser.RuleConfigModeSelect), pDevs, modes, base)
default:
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
}

View file

@ -1,6 +1,9 @@
package mappingrules 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. // A Simple Mapping Rule can map a button to a button or an axis to an axis.
type MappingRuleAxis struct { type MappingRuleAxis struct {
@ -9,12 +12,26 @@ type MappingRuleAxis struct {
Output *RuleTargetAxis 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{ return &MappingRuleAxis{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input,
Output: output, Output: output,
} }, nil
} }
func (rule *MappingRuleAxis) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleAxis) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -1,6 +1,7 @@
package mappingrules package mappingrules
import ( import (
"git.annabunches.net/annabunches/joyful/internal/configparser"
"git.annabunches.net/annabunches/joyful/internal/logger" "git.annabunches.net/annabunches/joyful/internal/logger"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
@ -12,7 +13,26 @@ type MappingRuleAxisCombined struct {
Output *RuleTargetAxis 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 inputLower.OutputMax = 0
inputUpper.OutputMin = 0 inputUpper.OutputMin = 0
return &MappingRuleAxisCombined{ return &MappingRuleAxisCombined{
@ -20,7 +40,7 @@ func NewMappingRuleAxisCombined(base MappingRuleBase, inputLower *RuleTargetAxis
InputLower: inputLower, InputLower: inputLower,
InputUpper: inputUpper, InputUpper: inputUpper,
Output: output, Output: output,
} }, nil
} }
func (rule *MappingRuleAxisCombined) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleAxisCombined) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -38,7 +38,9 @@ func (t *MappingRuleAxisCombinedTests) SetupTest() {
}, nil) }, nil)
t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, 0, 0) 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, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, 0, 0)
t.inputTargetUpper.OutputMin = 0
t.outputDevice = &evdev.InputDevice{} t.outputDevice = &evdev.InputDevice{}
t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0) t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0)
@ -57,19 +59,30 @@ func (t *MappingRuleAxisCombinedTests) TearDownSubTest() {
t.inputDevice.Reset() t.inputDevice.Reset()
} }
// TODO: this test sucks
func (t *MappingRuleAxisCombinedTests) TestNewMappingRuleAxisCombined() { func (t *MappingRuleAxisCombinedTests) TestNewMappingRuleAxisCombined() {
t.inputDevice.Stub("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{ t.inputDevice.Stub("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{
evdev.ABS_X: {Minimum: 0, Maximum: 10000}, evdev.ABS_X: {Minimum: 0, Maximum: 10000},
evdev.ABS_Y: {Minimum: 0, Maximum: 10000}, evdev.ABS_Y: {Minimum: 0, Maximum: 10000},
}, nil) }, 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.InputLower.OutputMax)
t.EqualValues(0, rule.InputUpper.OutputMin) t.EqualValues(0, rule.InputUpper.OutputMin)
} }
func (t *MappingRuleAxisCombinedTests) TestMatchEvent() { 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() { t.Run("Lower Input", func() {
testCases := []struct{ in, out int32 }{ testCases := []struct{ in, out int32 }{

View file

@ -3,6 +3,7 @@ package mappingrules
import ( import (
"time" "time"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
"github.com/jonboulle/clockwork" "github.com/jonboulle/clockwork"
) )
@ -23,20 +24,34 @@ type MappingRuleAxisToButton struct {
clock clockwork.Clock 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{ return &MappingRuleAxisToButton{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input,
Output: output, Output: output,
RepeatRateMin: repeatRateMin, RepeatRateMin: ruleConfig.RepeatRateMin,
RepeatRateMax: repeatRateMax, RepeatRateMax: ruleConfig.RepeatRateMax,
lastEvent: time.Now(), lastEvent: time.Now(),
nextEvent: NoNextEvent, nextEvent: NoNextEvent,
repeat: repeatRateMin != 0 && repeatRateMax != 0, repeat: ruleConfig.RepeatRateMin != 0 && ruleConfig.RepeatRateMax != 0,
pressed: false, pressed: false,
active: false, active: false,
clock: clockwork.NewRealClock(), clock: clockwork.NewRealClock(),
} }, nil
} }
func (rule *MappingRuleAxisToButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleAxisToButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -19,6 +19,44 @@ type MappingRuleAxisToButtonTests struct {
base MappingRuleBase 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() { func (t *MappingRuleAxisToButtonTests) SetupTest() {
mode := "*" mode := "*"
t.mode = &mode t.mode = &mode
@ -40,7 +78,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
// A valid input should set a nextevent // A valid input should set a nextevent
t.Run("No Repeat", func() { 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() { t.Run("Valid Input", func() {
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{ testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
@ -62,7 +100,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
}) })
t.Run("Repeat", func() { 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{ testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
Type: evdev.EV_ABS, Type: evdev.EV_ABS,
Code: evdev.ABS_X, Code: evdev.ABS_X,
@ -90,7 +128,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
t.Run("No Repeat", func() { t.Run("No Repeat", func() {
// Get event if called immediately // Get event if called immediately
t.Run("Event is available immediately", func() { t.Run("Event is available immediately", func() {
testRule, _ := buildTimerRule(t, 0, 0, 0) testRule, _ := t.buildTimerRule(0, 0, 0)
event := testRule.TimerEvent() event := testRule.TimerEvent()
@ -100,7 +138,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
// Off event on second call // Off event on second call
t.Run("Event emits off on second call", func() { t.Run("Event emits off on second call", func() {
testRule, _ := buildTimerRule(t, 0, 0, 0) testRule, _ := t.buildTimerRule(0, 0, 0)
testRule.TimerEvent() testRule.TimerEvent()
event := testRule.TimerEvent() event := testRule.TimerEvent()
@ -111,7 +149,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
// No further event, even if we wait a while // No further event, even if we wait a while
t.Run("Additional events are not emitted while still active.", func() { 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()
testRule.TimerEvent() testRule.TimerEvent()
@ -125,13 +163,13 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
t.Run("Repeat", func() { t.Run("Repeat", func() {
t.Run("No event if called immediately", 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() event := testRule.TimerEvent()
t.Nil(event) t.Nil(event)
}) })
t.Run("No event after 49ms", func() { 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) mockClock.Advance(49 * time.Millisecond)
event := testRule.TimerEvent() event := testRule.TimerEvent()
@ -140,7 +178,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
}) })
t.Run("Event after 50ms", func() { 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) mockClock.Advance(50 * time.Millisecond)
event := testRule.TimerEvent() event := testRule.TimerEvent()
@ -150,7 +188,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
}) })
t.Run("Additional event at 100ms", func() { 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) mockClock.Advance(50 * time.Millisecond)
testRule.TimerEvent() 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
}

View file

@ -3,6 +3,7 @@ package mappingrules
import ( import (
"time" "time"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
"github.com/jonboulle/clockwork" "github.com/jonboulle/clockwork"
) )
@ -23,23 +24,32 @@ type MappingRuleAxisToRelaxis struct {
clock clockwork.Clock clock clockwork.Clock
} }
func NewMappingRuleAxisToRelaxis( func NewMappingRuleAxisToRelaxis(ruleConfig configparser.RuleConfigAxisToRelaxis,
base MappingRuleBase, pDevs map[string]Device,
input *RuleTargetAxis, vDevs map[string]Device,
output *RuleTargetRelaxis, base MappingRuleBase) (*MappingRuleAxisToRelaxis, error) {
repeatRateMin, repeatRateMax, increment int) *MappingRuleAxisToRelaxis {
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{ return &MappingRuleAxisToRelaxis{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input,
Output: output, Output: output,
RepeatRateMin: repeatRateMin, RepeatRateMin: ruleConfig.RepeatRateMin,
RepeatRateMax: repeatRateMax, RepeatRateMax: ruleConfig.RepeatRateMax,
Increment: int32(increment), Increment: int32(ruleConfig.Increment),
lastEvent: time.Now(), lastEvent: time.Now(),
nextEvent: NoNextEvent, nextEvent: NoNextEvent,
clock: clockwork.NewRealClock(), clock: clockwork.NewRealClock(),
} }, nil
} }
func (rule *MappingRuleAxisToRelaxis) MatchEvent( func (rule *MappingRuleAxisToRelaxis) MatchEvent(

View file

@ -1,6 +1,9 @@
package mappingrules 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. // A Simple Mapping Rule can map a button to a button or an axis to an axis.
type MappingRuleButton struct { type MappingRuleButton struct {
@ -9,16 +12,26 @@ type MappingRuleButton struct {
Output *RuleTargetButton Output *RuleTargetButton
} }
func NewMappingRuleButton( func NewMappingRuleButton(ruleConfig configparser.RuleConfigButton,
base MappingRuleBase, pDevs map[string]Device,
input *RuleTargetButton, vDevs map[string]Device,
output *RuleTargetButton) *MappingRuleButton { 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{ return &MappingRuleButton{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input,
Output: output, Output: output,
} }, nil
} }
func (rule *MappingRuleButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -1,6 +1,9 @@
package mappingrules 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 // A Combo Mapping Rule can require multiple physical button presses for a single output button
type MappingRuleButtonCombo struct { type MappingRuleButtonCombo struct {
@ -10,17 +13,31 @@ type MappingRuleButtonCombo struct {
State int State int
} }
func NewMappingRuleButtonCombo( func NewMappingRuleButtonCombo(ruleConfig configparser.RuleConfigButtonCombo,
base MappingRuleBase, pDevs map[string]Device,
inputs []*RuleTargetButton, vDevs map[string]Device,
output *RuleTargetButton) *MappingRuleButtonCombo { 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{ return &MappingRuleButtonCombo{
MappingRuleBase: base, MappingRuleBase: base,
Inputs: inputs, Inputs: inputs,
Output: output, Output: output,
State: 0, State: 0,
} }, nil
} }
func (rule *MappingRuleButtonCombo) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleButtonCombo) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -1,6 +1,9 @@
package mappingrules package mappingrules
import "github.com/holoplot/go-evdev" import (
"git.annabunches.net/annabunches/joyful/internal/configparser"
"github.com/holoplot/go-evdev"
)
type MappingRuleButtonLatched struct { type MappingRuleButtonLatched struct {
MappingRuleBase MappingRuleBase
@ -9,17 +12,27 @@ type MappingRuleButtonLatched struct {
State bool State bool
} }
func NewMappingRuleButtonLatched( func NewMappingRuleButtonLatched(ruleConfig configparser.RuleConfigButtonLatched,
base MappingRuleBase, pDevs map[string]Device,
input *RuleTargetButton, vDevs map[string]Device,
output *RuleTargetButton) *MappingRuleButtonLatched { 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{ return &MappingRuleButtonLatched{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input,
Output: output, Output: output,
State: false, State: false,
} }, nil
} }
func (rule *MappingRuleButtonLatched) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleButtonLatched) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -28,7 +28,11 @@ func (t *MappingRuleButtonTests) SetupTest() {
func (t *MappingRuleButtonTests) TestMatchEvent() { func (t *MappingRuleButtonTests) TestMatchEvent() {
inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, false) inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, false)
outputButton, _ := NewRuleTargetButton("", t.outputDevice, 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 // A matching input event should produce an output event
expected := &evdev.InputEvent{ expected := &evdev.InputEvent{
@ -58,7 +62,11 @@ func (t *MappingRuleButtonTests) TestMatchEvent() {
func (t *MappingRuleButtonTests) TestMatchEventInverted() { func (t *MappingRuleButtonTests) TestMatchEventInverted() {
inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, true) inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, true)
outputButton, _ := NewRuleTargetButton("", t.outputDevice, 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 // A matching input event should produce an output event
expected := &evdev.InputEvent{ expected := &evdev.InputEvent{

View file

@ -1,6 +1,9 @@
package mappingrules package mappingrules
import "github.com/holoplot/go-evdev" import (
"git.annabunches.net/annabunches/joyful/internal/configparser"
"github.com/holoplot/go-evdev"
)
type MappingRuleModeSelect struct { type MappingRuleModeSelect struct {
MappingRuleBase MappingRuleBase
@ -8,17 +11,26 @@ type MappingRuleModeSelect struct {
Output *RuleTargetModeSelect Output *RuleTargetModeSelect
} }
func NewMappingRuleModeSelect( func NewMappingRuleModeSelect(ruleConfig configparser.RuleConfigModeSelect,
base MappingRuleBase, pDevs map[string]Device,
input *RuleTargetButton, modes []string,
output *RuleTargetModeSelect, base MappingRuleBase) (*MappingRuleModeSelect, error) {
) *MappingRuleModeSelect {
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{ return &MappingRuleModeSelect{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input,
Output: output, Output: output,
} }, nil
} }
func (rule *MappingRuleModeSelect) MatchEvent( func (rule *MappingRuleModeSelect) MatchEvent(

View file

@ -28,3 +28,16 @@ func Clamp[T Numeric](value, min, max T) T {
} }
return value 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
}

View file

@ -4,6 +4,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
@ -20,6 +22,77 @@ type RuleTargetAxis struct {
deadzoneSize int32 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, func NewRuleTargetAxis(device_name string,
device Device, device Device,
axis evdev.EvCode, axis evdev.EvCode,

View file

@ -1,6 +1,12 @@
package mappingrules 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 { type RuleTargetButton struct {
DeviceName string DeviceName string
@ -9,6 +15,25 @@ type RuleTargetButton struct {
Inverted bool 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) { func NewRuleTargetButton(device_name string, device Device, code evdev.EvCode, inverted bool) (*RuleTargetButton, error) {
return &RuleTargetButton{ return &RuleTargetButton{
DeviceName: device_name, DeviceName: device_name,

View file

@ -4,6 +4,7 @@ import (
"errors" "errors"
"slices" "slices"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"git.annabunches.net/annabunches/joyful/internal/logger" "git.annabunches.net/annabunches/joyful/internal/logger"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
@ -12,6 +13,14 @@ type RuleTargetModeSelect struct {
Modes []string 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) { func NewRuleTargetModeSelect(modes []string) (*RuleTargetModeSelect, error) {
if len(modes) == 0 { if len(modes) == 0 {
return nil, errors.New("cannot create RuleTargetModeSelect: mode list is empty") return nil, errors.New("cannot create RuleTargetModeSelect: mode list is empty")

View file

@ -1,6 +1,10 @@
package mappingrules package mappingrules
import ( import (
"fmt"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
@ -10,12 +14,30 @@ type RuleTargetRelaxis struct {
Axis evdev.EvCode Axis evdev.EvCode
} }
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, device Device,
axis evdev.EvCode) (*RuleTargetRelaxis, error) { axis evdev.EvCode) (*RuleTargetRelaxis, error) {
return &RuleTargetRelaxis{ return &RuleTargetRelaxis{
DeviceName: device_name, DeviceName: deviceName,
Device: device, Device: device,
Axis: axis, Axis: axis,
}, nil }, nil

View file

@ -0,0 +1,12 @@
package mappingrules
const (
RuleTypeButton = "button"
RuleTypeButtonCombo = "button-combo"
RuleTypeButtonLatched = "button-latched"
RuleTypeAxis = "axis"
RuleTypeAxisCombined = "axis-combined"
RuleTypeAxisToButton = "axis-to-button"
RuleTypeAxisToRelaxis = "axis-to-relaxis"
RuleTypeModeSelect = "mode-select"
)

View file

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

View file

@ -11,13 +11,7 @@ import (
type EventBuffer struct { type EventBuffer struct {
events []*evdev.InputEvent events []*evdev.InputEvent
Device VirtualDevice Device VirtualDevice
} Name string
func NewEventBuffer(device VirtualDevice) *EventBuffer {
return &EventBuffer{
events: make([]*evdev.InputEvent, 0, 100),
Device: device,
}
} }
func (buffer *EventBuffer) AddEvent(event *evdev.InputEvent) { func (buffer *EventBuffer) AddEvent(event *evdev.InputEvent) {

View file

@ -11,10 +11,11 @@ import (
type EventBufferTests struct { type EventBufferTests struct {
suite.Suite suite.Suite
device *VirtualDeviceMock device *VirtualDeviceMock
writeOneCall *mock.Call buffer *EventBuffer
} }
// Mocks
type VirtualDeviceMock struct { type VirtualDeviceMock struct {
mock.Mock mock.Mock
} }
@ -24,65 +25,65 @@ func (m *VirtualDeviceMock) WriteOne(event *evdev.InputEvent) error {
return args.Error(0) return args.Error(0)
} }
// Setup
func TestRunnerEventBufferTests(t *testing.T) { func TestRunnerEventBufferTests(t *testing.T) {
suite.Run(t, new(EventBufferTests)) suite.Run(t, new(EventBufferTests))
} }
func (t *EventBufferTests) SetupTest() {
t.device = new(VirtualDeviceMock)
}
func (t *EventBufferTests) SetupSubTest() { func (t *EventBufferTests) SetupSubTest() {
t.device = new(VirtualDeviceMock) t.device = new(VirtualDeviceMock)
t.writeOneCall = t.device.On("WriteOne").Return(nil) t.buffer = &EventBuffer{Device: t.device}
}
func (t *EventBufferTests) TearDownSubTest() {
t.writeOneCall.Unset()
} }
// Tests
func (t *EventBufferTests) TestNewEventBuffer() { func (t *EventBufferTests) TestNewEventBuffer() {
buffer := NewEventBuffer(t.device) t.Equal(t.device, t.buffer.Device)
t.Equal(t.device, buffer.Device) t.Len(t.buffer.events, 0)
t.Len(buffer.events, 0)
} }
func (t *EventBufferTests) TestEventBufferAddEvent() { func (t *EventBufferTests) TestEventBuffer() {
buffer := NewEventBuffer(t.device)
buffer.AddEvent(&evdev.InputEvent{}) t.Run("AddEvent", func() {
buffer.AddEvent(&evdev.InputEvent{}) t.buffer.AddEvent(&evdev.InputEvent{})
buffer.AddEvent(&evdev.InputEvent{}) t.buffer.AddEvent(&evdev.InputEvent{})
t.Len(buffer.events, 3) t.buffer.AddEvent(&evdev.InputEvent{})
} t.Len(t.buffer.events, 3)
})
func (t *EventBufferTests) TestEventBufferSendEvents() {
t.Run("3 Events", func() { t.Run("SendEvents", func() {
buffer := NewEventBuffer(t.device) t.Run("3 Events", func() {
buffer.AddEvent(&evdev.InputEvent{}) writeOneCall := t.device.On("WriteOne").Return(nil)
buffer.AddEvent(&evdev.InputEvent{})
buffer.AddEvent(&evdev.InputEvent{}) t.buffer.AddEvent(&evdev.InputEvent{})
errs := buffer.SendEvents() t.buffer.AddEvent(&evdev.InputEvent{})
t.buffer.AddEvent(&evdev.InputEvent{})
t.Len(errs, 0) errs := t.buffer.SendEvents()
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 4)
}) t.Len(errs, 0)
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 4)
t.Run("No Events", func() {
buffer := NewEventBuffer(t.device) writeOneCall.Unset()
errs := buffer.SendEvents() })
t.Len(errs, 0) t.Run("No Events", func() {
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 0) writeOneCall := t.device.On("WriteOne").Return(nil)
})
errs := t.buffer.SendEvents()
t.Run("Bad Event", func() {
t.writeOneCall.Unset() t.Len(errs, 0)
t.writeOneCall = t.device.On("WriteOne").Return(errors.New("Fail")) t.device.AssertNumberOfCalls(t.T(), "WriteOne", 0)
buffer := NewEventBuffer(t.device) writeOneCall.Unset()
buffer.AddEvent(&evdev.InputEvent{}) })
errs := buffer.SendEvents()
t.Len(errs, 2) 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()
})
})
} }

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

View file

@ -1,4 +1,4 @@
package config package virtualdevice
import ( import (
"testing" "testing"
@ -7,15 +7,15 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
type DevicesConfigTests struct { type InitTests struct {
suite.Suite suite.Suite
} }
func TestRunnerDevicesConfig(t *testing.T) { func TestRunnerInit(t *testing.T) {
suite.Run(t, new(DevicesConfigTests)) suite.Run(t, new(InitTests))
} }
func (t *DevicesConfigTests) TestMakeButtons() { func (t *InitTests) TestMakeButtons() {
t.Run("Maximum buttons", func() { t.Run("Maximum buttons", func() {
buttons := makeButtons(VirtualDeviceMaxButtons, []string{}) buttons := makeButtons(VirtualDeviceMaxButtons, []string{})
t.Equal(VirtualDeviceMaxButtons, len(buttons)) 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() { t.Run("8 axes", func() {
axes := makeAxes(8, []string{}) axes := makeAxes(8, []string{})
t.Equal(8, len(axes)) 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() { t.Run("10 axes", func() {
axes := makeRelativeAxes(10, []string{}) axes := makeRelativeAxes(10, []string{})
t.Equal(10, len(axes)) t.Equal(10, len(axes))

View file

@ -1,114 +1,16 @@
package config package virtualdevice
import ( import "github.com/holoplot/go-evdev"
"github.com/holoplot/go-evdev"
)
const ( const (
DeviceTypePhysical = "physical"
DeviceTypeVirtual = "virtual"
DevicePresetKeyboard = "keyboard" DevicePresetKeyboard = "keyboard"
DevicePresetGamepad = "gamepad" DevicePresetGamepad = "gamepad"
DevicePresetJoystick = "joystick" DevicePresetJoystick = "joystick"
DevicePresetMouse = "mouse" DevicePresetMouse = "mouse"
RuleTypeButton = "button"
RuleTypeButtonCombo = "button-combo"
RuleTypeButtonLatched = "button-latched"
RuleTypeAxis = "axis"
RuleTypeAxisCombined = "axis-combined"
RuleTypeAxisToButton = "axis-to-button"
RuleTypeAxisToRelaxis = "axis-to-relaxis"
RuleTypeModeSelect = "mode-select"
CodePrefixButton = "BTN"
CodePrefixKey = "KEY"
CodePrefixAxis = "ABS"
CodePrefixRelaxis = "REL"
VirtualDeviceMaxButtons = 74 VirtualDeviceMaxButtons = 74
) )
var (
ButtonFromIndex = []evdev.EvCode{
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),
}
)
// Device Presets // Device Presets
var ( var (
CapabilitiesPresetGamepad = map[evdev.EvType][]evdev.EvCode{ CapabilitiesPresetGamepad = map[evdev.EvType][]evdev.EvCode{