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

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

@ -0,0 +1,225 @@
// These types comprise the YAML schema for configuring Joyful.
// The config files will be combined and then unmarshalled into this
package configparser
import (
"fmt"
)
type Config struct {
Devices []DeviceConfig
Modes []string
Rules []RuleConfig
}
// These top-level structs use custom unmarshaling to unpack each available sub-type
type DeviceConfig struct {
Type string
Config interface{}
}
type RuleConfig struct {
Type string
Name string
Modes []string
Config interface{}
}
type DeviceConfigPhysical struct {
Name string
DeviceName string `yaml:"device_name,omitempty"`
DevicePath string `yaml:"device_path,omitempty"`
Lock bool
}
// TODO: configure custom unmarshaling so we can overload Buttons, Axes, and RelativeAxes...
type DeviceConfigVirtual struct {
Name string
Preset string
NumButtons int `yaml:"num_buttons,omitempty"`
NumAxes int `yaml:"num_axes,omitempty"`
NumRelativeAxes int `yaml:"num_rel_axes"`
Buttons []string
Axes []string
RelativeAxes []string `yaml:"rel_axes,omitempty"`
}
type RuleConfigButton struct {
Input RuleTargetConfigButton
Output RuleTargetConfigButton
}
type RuleConfigButtonCombo struct {
Inputs []RuleTargetConfigButton
Output RuleTargetConfigButton
}
type RuleConfigButtonLatched struct {
Input RuleTargetConfigButton
Output RuleTargetConfigButton
}
type RuleConfigAxis struct {
Input RuleTargetConfigAxis
Output RuleTargetConfigAxis
}
type RuleConfigAxisCombined struct {
InputLower RuleTargetConfigAxis `yaml:"input_lower,omitempty"`
InputUpper RuleTargetConfigAxis `yaml:"input_upper,omitempty"`
Output RuleTargetConfigAxis
}
type RuleConfigAxisToButton struct {
RepeatRateMin int `yaml:"repeat_rate_min,omitempty"`
RepeatRateMax int `yaml:"repeat_rate_max,omitempty"`
Input RuleTargetConfigAxis
Output RuleTargetConfigButton
}
type RuleConfigAxisToRelaxis struct {
RepeatRateMin int `yaml:"repeat_rate_min"`
RepeatRateMax int `yaml:"repeat_rate_max"`
Increment int
Input RuleTargetConfigAxis
Output RuleTargetConfigRelaxis
}
type RuleConfigModeSelect struct {
Input RuleTargetConfigButton
Output RuleTargetConfigModeSelect
}
type RuleTargetConfigButton struct {
Device string
Button string
Inverted bool
}
type RuleTargetConfigAxis struct {
Device string
Axis string
DeadzoneCenter int32 `yaml:"deadzone_center,omitempty"`
DeadzoneSize int32 `yaml:"deadzone_size,omitempty"`
DeadzoneSizePercent int32 `yaml:"deadzone_size_percent,omitempty"`
DeadzoneStart int32 `yaml:"deadzone_start,omitempty"`
DeadzoneEnd int32 `yaml:"deadzone_end,omitempty"`
Inverted bool
}
type RuleTargetConfigRelaxis struct {
Device string
Axis string
}
type RuleTargetConfigModeSelect struct {
Modes []string
}
func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
metaConfig := &struct {
Type string
}{}
err := unmarshal(metaConfig)
if err != nil {
return err
}
dc.Type = metaConfig.Type
err = nil
switch metaConfig.Type {
case DeviceTypePhysical:
config := DeviceConfigPhysical{}
err = unmarshal(&config)
dc.Config = config
case DeviceTypeVirtual:
config := DeviceConfigVirtual{}
err = unmarshal(&config)
dc.Config = config
default:
err = fmt.Errorf("invalid device type '%s'", dc.Type)
}
return err
}
func (dc *RuleConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
metaConfig := &struct {
Type string
Name string
Modes []string
}{}
err := unmarshal(metaConfig)
if err != nil {
return err
}
dc.Type = metaConfig.Type
dc.Name = metaConfig.Name
dc.Modes = metaConfig.Modes
switch dc.Type {
case RuleTypeButton:
config := RuleConfigButton{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeButtonCombo:
config := RuleConfigButtonCombo{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeButtonLatched:
config := RuleConfigButtonLatched{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeAxis:
config := RuleConfigAxis{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeAxisCombined:
config := RuleConfigAxisCombined{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeAxisToButton:
config := RuleConfigAxisToButton{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeAxisToRelaxis:
config := RuleConfigAxisToRelaxis{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeModeSelect:
config := RuleConfigModeSelect{}
err = unmarshal(&config)
dc.Config = config
default:
err = fmt.Errorf("invalid rule type '%s'", dc.Type)
}
return err
}
// TODO: custom yaml unmarshaling is obtuse; do we really need to do all of this work
// just to set a single default value?
func (dc *DeviceConfigPhysical) UnmarshalYAML(unmarshal func(data interface{}) error) error {
var raw struct {
Name string
DeviceName string `yaml:"device_name"`
DevicePath string `yaml:"device_path"`
Lock bool `yaml:"lock,omitempty"`
}
// Set non-standard defaults
raw.Lock = true
err := unmarshal(&raw)
if err != nil {
return err
}
*dc = DeviceConfigPhysical{
Name: raw.Name,
DeviceName: raw.DeviceName,
DevicePath: raw.DevicePath,
Lock: raw.Lock,
}
return nil
}

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"
)