From faa51bdda2a98b388bc44f9724cc7601f1b171ad Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Tue, 1 Jul 2025 11:27:14 -0400 Subject: [PATCH] Implement basic config file parsing and create virtual devices from config file. --- cmd/joyful/main.go | 39 ++++++--- internal/config/configparser.go | 102 ++++++++++++++-------- internal/config/schema.go | 41 +++++++++ internal/config/types.go | 42 --------- internal/config/{maps.go => variables.go} | 8 ++ internal/logger/logger.go | 12 ++- 6 files changed, 156 insertions(+), 88 deletions(-) create mode 100644 internal/config/schema.go delete mode 100644 internal/config/types.go rename internal/config/{maps.go => variables.go} (91%) diff --git a/cmd/joyful/main.go b/cmd/joyful/main.go index 3abb9b8..94d71a9 100644 --- a/cmd/joyful/main.go +++ b/cmd/joyful/main.go @@ -2,6 +2,8 @@ package main import ( "fmt" + "os" + "path/filepath" "time" "git.annabunches.net/annabunches/joyful/internal/config" @@ -10,18 +12,36 @@ import ( "github.com/holoplot/go-evdev" ) -func main() { - // parse configs +func readConfig() *config.ConfigParser { parser := &config.ConfigParser{} - parser.Parse("~/.config/joyful") // TODO: make ~ work here + homeDir, err := os.UserHomeDir() + logger.FatalIfError(err, "Can't get user home directory, so can't find configuration.") + err = parser.Parse(filepath.Join(homeDir, ".config/joyful")) + logger.FatalIfError(err, "") + return parser +} - vDevices := parser.CreateVirtualDevices() - vBuffers := make(map[string]*virtualdevice.EventBuffer) +func initVirtualDevices(config *config.ConfigParser) map[string]*virtualdevice.EventBuffer { + vDevices := config.CreateVirtualDevices() + vBuffers := make(map[string]*virtualdevice.EventBuffer) for name, device := range vDevices { vBuffers[name] = virtualdevice.NewEventBuffer(device) } + return vBuffers +} - pDevice, err := evdev.Open("/dev/input/event12") +func main() { + // parse configs + config := readConfig() + + // Initialize virtual devices and event buffers + vBuffers := initVirtualDevices(config) + + // Initialize physical devices + // pDevices := config.ConnectPhysicalDevices() + + // TEST CODE + pDevice, err := evdev.Open("/dev/input/event11") logger.FatalIfError(err, "Couldn't open physical device") name, err := pDevice.Name() @@ -31,14 +51,13 @@ func main() { fmt.Printf("Connected to physical device %s\n", name) var combo int32 = 0 - buffer := vBuffers["test"] + buffer := vBuffers["main"] for { last := combo event, err := pDevice.ReadOne() logger.LogIfError(err, "Error while reading event") - // FIXME: test code for event.Code != evdev.SYN_REPORT { if event.Type == evdev.EV_KEY { switch event.Code { @@ -95,8 +114,8 @@ func main() { } buffer.SendEvents() - // FIXME: end test code time.Sleep(1 * time.Millisecond) } -} \ No newline at end of file + // END TEST CODE +} diff --git a/internal/config/configparser.go b/internal/config/configparser.go index cc94590..b837d66 100644 --- a/internal/config/configparser.go +++ b/internal/config/configparser.go @@ -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 -} \ No newline at end of file +} diff --git a/internal/config/schema.go b/internal/config/schema.go new file mode 100644 index 0000000..43e95b8 --- /dev/null +++ b/internal/config/schema.go @@ -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"` +} diff --git a/internal/config/types.go b/internal/config/types.go deleted file mode 100644 index 2e78cc6..0000000 --- a/internal/config/types.go +++ /dev/null @@ -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"` -} diff --git a/internal/config/maps.go b/internal/config/variables.go similarity index 91% rename from internal/config/maps.go rename to internal/config/variables.go index fdab86d..caf6691 100644 --- a/internal/config/maps.go +++ b/internal/config/variables.go @@ -60,3 +60,11 @@ var ( evdev.BTN_TRIGGER_HAPPY40, } ) + +const ( + DeviceTypePhysical = "physical" + DeviceTypeVirtual = "virtual" + + RuleTypeSimple = "simple" + RuleTypeCombo = "combo" +) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 5b16c7b..31e8141 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -9,12 +9,20 @@ func Log(msg string) { fmt.Println(msg) } +func LogError(err error, msg string) { + if msg == "" { + fmt.Printf("%s\n", err.Error()) + } else { + fmt.Printf("%s: %s\n", msg, err.Error()) + } +} + func LogIfError(err error, msg string) { if err == nil { return } - fmt.Printf("%s: %s\n", msg, err.Error()) + LogError(err, msg) } func FatalIfError(err error, msg string) { @@ -22,6 +30,6 @@ func FatalIfError(err error, msg string) { return } - LogIfError(err, msg) + LogError(err, msg) os.Exit(1) }