Implement basic config file parsing and create virtual devices from config file.

This commit is contained in:
Anna Rose Wiggins 2025-07-01 11:27:14 -04:00
parent eeebb6d0b1
commit faa51bdda2
6 changed files with 156 additions and 88 deletions

View file

@ -1,6 +1,7 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
@ -12,19 +13,23 @@ import (
)
type ConfigParser struct {
config Config
configFiles []string
config Config
}
// Parse all the config files and store the config data for further use
func (parser *ConfigParser) Parse(directory string) {
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)
logger.FatalIfError(err, "Failed to create config directory at "+directory)
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")) {
@ -33,42 +38,41 @@ func (parser *ConfigParser) Parse(directory string) {
filePath := filepath.Join(directory, name)
if strings.HasSuffix(filePath, ".yaml") || strings.HasSuffix(filePath, ".yml") {
parser.configFiles = append(parser.configFiles, filePath)
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.Groups = append(parser.config.Groups, newConfig.Groups...)
}
}
rawData := parser.parseConfigFiles()
err = yaml.Unmarshal(rawData, &parser.config)
logger.FatalIfError(err, "Failed to parse config")
}
// Open each config file and concatenate their contents
func (parser *ConfigParser) parseConfigFiles() []byte {
var rawData []byte
for _, filePath := range parser.configFiles {
data, err := os.ReadFile(filePath)
if err != nil {
logger.LogIfError(err, fmt.Sprintf("Failed to read config file '%s'", filePath))
continue
}
rawData = append(rawData, data...)
rawData = append(rawData, '\n')
if len(parser.config.Devices) == 0 {
return errors.New("Found no devices in configuration. Please add configuration at " + directory)
}
if len(rawData) == 0 {
logger.Log("No config data found. Write .yml config files in ~/.config/joyful")
return nil
}
return rawData
return nil
}
// 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.Virtual {
for _, deviceConfig := range parser.config.Devices {
if strings.ToLower(deviceConfig.Type) != DeviceTypeVirtual {
continue
}
vDevice, err := evdev.CreateDevice(
fmt.Sprintf("joyful-%s", deviceConfig.Name),
// TODO: who knows what these should actually be
@ -95,12 +99,42 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice
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.
//
// STUB: this function does not yet function.
func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevice {
deviceMap := make(map[string]*evdev.InputDevice)
for _, deviceConfig := range parser.config.Devices {
if strings.ToLower(deviceConfig.Type) != DeviceTypePhysical {
continue
}
vDevice, err := evdev.Open("/dev/input/foo")
if err != nil {
logger.LogIfError(err, "Failed to open physical device")
continue
}
deviceMap[deviceConfig.Name] = vDevice
}
return deviceMap
}
func makeButtons(numButtons int) []evdev.EvCode {
if numButtons > 56 {
numButtons = 56
logger.Log("Limiting virtual device buttons to 56")
}
buttons := make([]evdev.EvCode, numButtons)
startCode := 0x120
@ -110,8 +144,8 @@ func makeButtons(numButtons int) []evdev.EvCode {
if numButtons > 16 {
startCode = 0x2c0
for i := 0; i < numButtons - 16; i++ {
buttons[16+i] = evdev.EvCode(startCode+i)
for i := 0; i < numButtons-16; i++ {
buttons[16+i] = evdev.EvCode(startCode + i)
}
}
@ -130,4 +164,4 @@ func makeAxes(numAxes int) []evdev.EvCode {
}
return axes
}
}

41
internal/config/schema.go Normal file
View file

@ -0,0 +1,41 @@
// These types comprise the YAML schema for configuring Joyful.
// The config files will be combined and then unmarshalled into this
package config
type Config struct {
Devices []DeviceConfig `yaml:"devices"`
// TODO: add groups
// Groups []GroupConfig `yaml:"groups,omitempty"`
Rules []RuleConfig `yaml:"rules"`
}
type DeviceConfig struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Uuid string `yaml:"uuid,omitempty"`
Buttons int `yaml:"buttons,omitempty"`
Axes int `yaml:"axes,omitempty"`
}
type RuleConfig struct {
Name string `yaml:"name,omitempty"`
Type string `yaml:"type"`
Input []RuleInputConfig `yaml:"input"`
Output RuleOutputConfig `yaml:"output"`
}
type RuleInputConfig struct {
Device string `yaml:"device"`
Button string `yaml:"button,omitempty"`
Buttons []string `yaml:"buttons,omitempty"`
Axis string `yaml:"axis,omitempty"`
Inverted bool `yaml:"inverted,omitempty"`
}
type RuleOutputConfig struct {
Device string `yaml:"device,omitempty"`
Button string `yaml:"button,omitempty"`
Axis string `yaml:"axis,omitempty"`
Groups string `yaml:"groups,omitempty"`
}

View file

@ -1,42 +0,0 @@
package config
type Config struct {
Devices DevicesConfig `yaml:"devices"`
Rules []RuleConfig `yaml:"rules"`
}
type DevicesConfig struct {
Physical []PhysicalDeviceConfig `yaml:"physical"`
Virtual []VirtualDeviceConfig `yaml:"virtual"`
}
type PhysicalDeviceConfig struct {
Name string `yaml:"name"`
Uuid string `yaml:"uuid"`
}
type VirtualDeviceConfig struct {
Name string `yaml:"name"`
Buttons int `yaml:"buttons"`
Axes int `yaml:"axes"`
}
type RuleConfig struct {
Type string `yaml:"type"`
Input RuleInputConfig `yaml:"input"`
Output RuleOutputConfig `yaml:"output"`
}
type RuleInputConfig struct {
Device string `yaml:"device"`
Button string `yaml:"button,omitempty"`
Buttons []string `yaml:"buttons,omitempty"`
Axis string `yaml:"axis,omitempty"`
Inverted bool `yaml:"inverted,omitempty"`
}
type RuleOutputConfig struct {
Device string `yaml:"device"`
Button string `yaml:"button,omitempty"`
Axis string `yaml:"axis,omitempty"`
}

View file

@ -60,3 +60,11 @@ var (
evdev.BTN_TRIGGER_HAPPY40,
}
)
const (
DeviceTypePhysical = "physical"
DeviceTypeVirtual = "virtual"
RuleTypeSimple = "simple"
RuleTypeCombo = "combo"
)