From af21756cefa7e670ec31c2089e1fcfdea0ab5522 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Tue, 5 Aug 2025 14:45:29 -0400 Subject: [PATCH 1/9] First sketch of a Rust re-implementation. --- .gitignore | 6 +- Cargo.lock | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 7 ++ src/main.rs | 230 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index d163863..57d0e8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -build/ \ No newline at end of file +build/ + +# Added by cargo + +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..76da654 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,244 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.5.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "joyful" +version = "0.1.0" +dependencies = [ + "clap", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5535819 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "joyful" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5.42", features = ["derive"] } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9373da7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,230 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[arg(short, long, default_value = "~/.config/joyful/")] + config: String, + + #[arg(short, long)] + debug: bool, + + #[arg(long, default_value_t = 100)] + tts_volume: u8, + + #[arg(long, default_value_t = 50)] + tts_pitch: u8, + + #[arg(long, default_value_t = 50)] + tts_range: u8, + + #[arg(long, default_value_t = 175)] + tts_speed: u8, + + #[arg(long, default_value = "en")] + tts_voice: String, +} + +fn main() { + // Parse Command-line + let args = Args::parse(); + + // Parse configs + + // Initialize TTS + + // Create Virtual Devices + + // Create Physical Devices + + // Create Rules + + // Create listening threads? + + // Loop: Parse Input +} + +// package main + +// import ( +// "context" +// "fmt" +// "os" +// "strings" +// "sync" + +// "github.com/holoplot/go-evdev" +// flag "github.com/spf13/pflag" + +// "git.annabunches.net/annabunches/joyful/internal/config" +// "git.annabunches.net/annabunches/joyful/internal/logger" +// "git.annabunches.net/annabunches/joyful/internal/mappingrules" +// "git.annabunches.net/annabunches/joyful/internal/virtualdevice" +// ) + +// func getConfigDir(dir string) string { +// configDir := strings.ReplaceAll(dir, "~", "${HOME}") +// return os.ExpandEnv(configDir) +// } + +// func readConfig(configDir string) *config.ConfigParser { +// parser := &config.ConfigParser{} +// err := parser.Parse(configDir) +// logger.FatalIfError(err, "Failed to parse config") +// return parser +// } + +// func initVirtualBuffers(config *config.ConfigParser) (map[string]*virtualdevice.EventBuffer, map[*evdev.InputDevice]*virtualdevice.EventBuffer) { +// vDevices := config.CreateVirtualDevices() +// if len(vDevices) == 0 { +// logger.Log("Warning: no virtual devices found in configuration. No rules will work.") +// } + +// vBuffersByName := make(map[string]*virtualdevice.EventBuffer) +// vBuffersByDevice := make(map[*evdev.InputDevice]*virtualdevice.EventBuffer) +// for name, device := range vDevices { +// vBuffersByName[name] = virtualdevice.NewEventBuffer(device) +// vBuffersByDevice[device] = vBuffersByName[name] +// } +// return vBuffersByName, vBuffersByDevice +// } + +// // Extracts the evdev devices from a list of virtual buffers and returns them. +// func getVirtualDevices(buffers map[string]*virtualdevice.EventBuffer) map[string]*evdev.InputDevice { +// devices := make(map[string]*evdev.InputDevice) +// for name, buffer := range buffers { +// devices[name] = buffer.Device.(*evdev.InputDevice) +// } +// return devices +// } + +// func initPhysicalDevices(config *config.ConfigParser) map[string]*evdev.InputDevice { +// pDeviceMap := config.ConnectPhysicalDevices() +// if len(pDeviceMap) == 0 { +// logger.Log("Warning: no physical devices found in configuration. No rules will work.") +// } +// return pDeviceMap +// } + +// func main() { +// // parse command-line +// var configFlag string +// flag.BoolVarP(&logger.IsDebugMode, "debug", "d", false, "Output very verbose debug messages.") +// flag.StringVarP(&configFlag, "config", "c", "~/.config/joyful", "Directory to read configuration from.") +// ttsOps := addTTSFlags() +// flag.Parse() + +// // parse configs +// configDir := getConfigDir(configFlag) +// config := readConfig(configDir) + +// // initialize TTS +// tts, err := newTTS(ttsOps) +// logger.LogIfError(err, "Failed to initialize TTS") + +// // Initialize virtual devices with event buffers +// vBuffersByName, vBuffersByDevice := initVirtualBuffers(config) + +// // Initialize physical devices +// pDevices := initPhysicalDevices(config) + +// // Load the rules +// rules, eventChannel, cancel, wg := loadRules(config, pDevices, getVirtualDevices(vBuffersByName)) + +// // initialize the mode variable +// mode := config.GetModes()[0] + +// // initialize TTS phrases for modes +// for _, m := range config.GetModes() { +// tts.AddMessage(m) +// logger.LogDebugf("Added TTS message '%s'", m) +// } + +// fmt.Println("Joyful Running! Press Ctrl+C to quit. Press Enter to reload rules.") +// if len(config.GetModes()) > 1 { +// logger.Logf("Initial mode set to '%s'", mode) +// } + +// for { +// lastMode := mode +// // Get an event (blocks if necessary) +// channelEvent := <-eventChannel + +// switch channelEvent.Type { +// case ChannelEventInput: +// switch channelEvent.Event.Type { +// case evdev.EV_SYN: +// // We've received a SYN_REPORT, so now we send all pending events; since SYN_REPORTs +// // might come from multiple input devices, we'll always flush, just to be sure. +// for _, buffer := range vBuffersByName { +// buffer.SendEvents() +// } + +// case evdev.EV_KEY, evdev.EV_ABS: +// // We have a matchable event type. Check all the events +// for _, rule := range rules { +// device, outputEvent := rule.MatchEvent(channelEvent.Device, channelEvent.Event, &mode) +// if device == nil || outputEvent == nil { +// continue +// } +// vBuffersByDevice[device].AddEvent(outputEvent) +// } +// } + +// case ChannelEventTimer: +// // Timer events give us the device and event to use directly +// vBuffersByDevice[channelEvent.Device].AddEvent(channelEvent.Event) +// // If we get a timer event, flush the output device buffer immediately +// vBuffersByDevice[channelEvent.Device].SendEvents() + +// case ChannelEventReload: +// // stop existing channels +// fmt.Println("Reloading rules.") +// cancel() +// fmt.Println("Waiting for existing listeners to exit. Provide input from each of your devices.") +// wg.Wait() +// fmt.Println("Listeners exited. Parsing config.") +// config := readConfig(configDir) // reload the config +// rules, eventChannel, cancel, wg = loadRules(config, pDevices, getVirtualDevices(vBuffersByName)) +// fmt.Println("Config re-loaded. Only rule changes applied. Device and Mode changes require restart.") +// } + +// if lastMode != mode && tts != nil { +// tts.Say(mode) +// } +// } +// } + +// func loadRules( +// config *config.ConfigParser, +// pDevices map[string]*evdev.InputDevice, +// vDevices map[string]*evdev.InputDevice) ([]mappingrules.MappingRule, <-chan ChannelEvent, func(), *sync.WaitGroup) { + +// var wg sync.WaitGroup +// eventChannel := make(chan ChannelEvent, 1000) +// ctx, cancel := context.WithCancel(context.Background()) + +// // Initialize rules +// rules := config.BuildRules(pDevices, vDevices) +// logger.Logf("Created %d mapping rules.", len(rules)) + +// // start listening for events on devices and timers +// for _, device := range pDevices { +// wg.Add(1) +// go eventWatcher(device, eventChannel, ctx, &wg) +// } + +// timerCount := 0 +// for _, rule := range rules { +// if timedRule, ok := rule.(mappingrules.TimedEventEmitter); ok { +// wg.Add(1) +// go timerWatcher(timedRule, eventChannel, ctx, &wg) +// timerCount++ +// } +// } +// logger.Logf("Registered %d timers.", timerCount) + +// go consoleWatcher(eventChannel) + +// return rules, eventChannel, cancel, &wg +// } From 08aac599a60ecdab62ff27e86acda25d01be34bb Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Wed, 6 Aug 2025 15:00:50 -0400 Subject: [PATCH 2/9] More rust. I have no idea what I'm doing and I am beginning to think golang is better. --- Cargo.lock | 237 +++++++++++++++++++++++++++++++++ Cargo.toml | 3 + src/bin/evinfo.rs | 29 ++++ src/{main.rs => bin/joyful.rs} | 50 ++++++- 4 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 src/bin/evinfo.rs rename src/{main.rs => bin/joyful.rs} (83%) diff --git a/Cargo.lock b/Cargo.lock index 76da654..bbb9cf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,47 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.5.42" @@ -98,6 +139,56 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "evdev" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c10865aeab1a7399b3c2d6046e8dcc7f5227b656f235ed63ef5ee45a47b8f8" +dependencies = [ + "bitvec", + "cfg-if", + "libc", + "nix", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "heck" version = "0.5.0" @@ -115,6 +206,42 @@ name = "joyful" version = "0.1.0" dependencies = [ "clap", + "evdev", + "shellexpand", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", ] [[package]] @@ -123,6 +250,21 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" +dependencies = [ + "memchr", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -141,6 +283,60 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shellexpand" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" +dependencies = [ + "bstr", + "dirs", + "os_str_bytes", +] + [[package]] name = "strsim" version = "0.11.1" @@ -158,6 +354,32 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -170,6 +392,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "windows-sys" version = "0.59.0" @@ -242,3 +470,12 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] diff --git a/Cargo.toml b/Cargo.toml index 5535819..a380d4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "joyful" version = "0.1.0" +description = "Joystick remapper" edition = "2024" [dependencies] clap = { version = "4.5.42", features = ["derive"] } +evdev = { version = "0.13.1" } +shellexpand = { version = "3.1.1", features = ["full"] } diff --git a/src/bin/evinfo.rs b/src/bin/evinfo.rs new file mode 100644 index 0000000..c392f44 --- /dev/null +++ b/src/bin/evinfo.rs @@ -0,0 +1,29 @@ +use std::io; + +use clap::Parser; +use evdev::Device; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[arg(short, long)] + verbose: bool, +} + +fn main() { + let args = Args::parse(); + let mut _verbosity = 0; + if args.verbose { + _verbosity += 1; + } + + let devices = get_devices(); + + for _device in devices.iter() { + // print_device(device); + } +} + +fn get_devices() -> Vec { + return Vec::new(); +} diff --git a/src/main.rs b/src/bin/joyful.rs similarity index 83% rename from src/main.rs rename to src/bin/joyful.rs index 9373da7..5e26148 100644 --- a/src/main.rs +++ b/src/bin/joyful.rs @@ -1,26 +1,40 @@ +use std::env; +use std::error; +use std::fs; + use clap::Parser; +use shellexpand; + +type Result = std::result::Result>; #[derive(Parser, Debug)] -#[command(version, about, long_about = None)] +#[command(version, about, long_about)] struct Args { + /// The directory that contains your YAML configuration. #[arg(short, long, default_value = "~/.config/joyful/")] config: String, + /// Print extra information to the console. #[arg(short, long)] debug: bool, + /// Volume for text-to-speech. (0-200) #[arg(long, default_value_t = 100)] tts_volume: u8, + /// Median pitch for text-to-speech. (0-100) #[arg(long, default_value_t = 50)] tts_pitch: u8, + /// Pitch range for text-to-speech. (0-100) #[arg(long, default_value_t = 50)] tts_range: u8, + /// Speaking speed for text-to-speech in words per minute. #[arg(long, default_value_t = 175)] tts_speed: u8, + /// The espeak-ng voice to use for text-to-speech. #[arg(long, default_value = "en")] tts_voice: String, } @@ -30,6 +44,7 @@ fn main() { let args = Args::parse(); // Parse configs + let config_files = get_config_files(args.config); // Initialize TTS @@ -44,6 +59,39 @@ fn main() { // Loop: Parse Input } +fn get_config_files(config_dir: String) -> Result> { + let config_dir = shellexpand::full(&config_dir)?; + let paths = fs::read_dir(config_dir)?; + + let mut files: Vec = Vec::new(); + + for path in paths { + let path = match path { + Ok(path) => { + // DEBUG + println!("{:?}", path.path()); + path.path() + } + Err(err) => { + println!("{err}"); + continue; + } + }; + + if path.ends_with("yml") || path.ends_with("yaml") { + let path = match path.to_str() { + Some(path) => path, + None => { + continue; + } + }; + files.push(path.to_string()); + } + } + + return files; +} + // package main // import ( From 3f3382ffa7360fcfae8d80b514784f2f8a6b7c09 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Thu, 7 Aug 2025 19:02:55 -0400 Subject: [PATCH 3/9] Make evinfo build. --- src/bin/evinfo.rs | 50 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/bin/evinfo.rs b/src/bin/evinfo.rs index c392f44..82665f1 100644 --- a/src/bin/evinfo.rs +++ b/src/bin/evinfo.rs @@ -1,29 +1,49 @@ -use std::io; +use std::path::PathBuf; use clap::Parser; -use evdev::Device; +use evdev::raw_stream::RawDevice; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { - #[arg(short, long)] - verbose: bool, + /// Print additional information + #[arg(short, long, action = clap::ArgAction::Count)] + verbose: u8, } fn main() { let args = Args::parse(); - let mut _verbosity = 0; - if args.verbose { - _verbosity += 1; - } + let devices = evdev::raw_stream::enumerate(); + devices.for_each(|(path, dev)| match print_device(path, dev, args.verbose) { + Ok(_) => return, + Err(err) => println!("Failed to print device info: {err}"), + }); - let devices = get_devices(); - - for _device in devices.iter() { - // print_device(device); - } + println!("{}", args.verbose); } -fn get_devices() -> Vec { - return Vec::new(); +fn print_device(path: PathBuf, device: RawDevice, verbose: u8) -> Result<(), std::io::Error> { + println!( + "{}: \"{}\"", + path.to_str().unwrap_or_default(), + device.name().unwrap_or_default() + ); + + if verbose > 0 { + let input_id = device.input_id(); + println!("\tUUID:\t{}", device.unique_name().unwrap_or_default()); + println!("\tVendor:\t{}", input_id.vendor()); + println!("\tProduct:\t{}", input_id.product()); + println!("\tVersion:\t{}", input_id.version()); + } + + // if verbose > 1 { + // let absInfo = device.get_absinfo()?; + // if absInfo.count() > 0 { + // println!("\tAxis Data:"); + // absInfo.for_each(|info| println!("\t\t{} {}")); + // } + // } + + Ok(()) } From 63824510a5133dc15ed8c3cd45321c47c98e8da6 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Fri, 8 Aug 2025 10:21:48 -0400 Subject: [PATCH 4/9] Begin implementing is_joystick_like --- src/bin/evinfo.rs | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/bin/evinfo.rs b/src/bin/evinfo.rs index 82665f1..da52482 100644 --- a/src/bin/evinfo.rs +++ b/src/bin/evinfo.rs @@ -6,7 +6,7 @@ use evdev::raw_stream::RawDevice; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { - /// Print additional information + /// Print additional information about each device. (-vv for even more verbosity) #[arg(short, long, action = clap::ArgAction::Count)] verbose: u8, } @@ -14,15 +14,35 @@ struct Args { fn main() { let args = Args::parse(); let devices = evdev::raw_stream::enumerate(); - devices.for_each(|(path, dev)| match print_device(path, dev, args.verbose) { - Ok(_) => return, - Err(err) => println!("Failed to print device info: {err}"), + devices.for_each(|(path, dev)| { + if is_joystick_like(&dev) { + print_device(path, dev, args.verbose) + } }); - - println!("{}", args.verbose); } -fn print_device(path: PathBuf, device: RawDevice, verbose: u8) -> Result<(), std::io::Error> { +// Todo: can use a macro here... +const JOYSTICK_BUTTONS: Vec { + evdev::KeyCode::BTN_TRIGGER_HAPPY1, + evdev::KeyCode::BTN_TRIGGER_HAPPY2, + evdev::KeyCode::BTN_TRIGGER_HAPPY3, + evdev::KeyCode::BTN_TRIGGER_HAPPY4, + evdev::KeyCode::BTN_TRIGGER_HAPPY5, + evdev::KeyCode::BTN_TRIGGER_HAPPY6, + evdev::KeyCode::BTN_TRIGGER_HAPPY7, + evdev::KeyCode::BTN_TRIGGER_HAPPY8, + evdev::KeyCode::BTN_TRIGGER_HAPPY9, + evdev::KeyCode::BTN_TRIGGER_HAPPY10, +} + +fn is_joystick_like(device: &RawDevice) -> bool { + + if device.supported_absolute_axes().map_or(false, |axes| axes.contains()); + + return false; +} + +fn print_device(path: PathBuf, device: RawDevice, verbose: u8) { println!( "{}: \"{}\"", path.to_str().unwrap_or_default(), @@ -31,10 +51,10 @@ fn print_device(path: PathBuf, device: RawDevice, verbose: u8) -> Result<(), std if verbose > 0 { let input_id = device.input_id(); - println!("\tUUID:\t{}", device.unique_name().unwrap_or_default()); - println!("\tVendor:\t{}", input_id.vendor()); - println!("\tProduct:\t{}", input_id.product()); - println!("\tVersion:\t{}", input_id.version()); + println!("\tUUID:\t\t'{}'", device.unique_name().unwrap_or_default()); + println!("\tVendor:\t\t'{:x}'", input_id.vendor()); + println!("\tProduct:\t'{:x}'", input_id.product()); + println!("\tVersion:\t'{}'", input_id.version()); } // if verbose > 1 { @@ -44,6 +64,4 @@ fn print_device(path: PathBuf, device: RawDevice, verbose: u8) -> Result<(), std // absInfo.for_each(|info| println!("\t\t{} {}")); // } // } - - Ok(()) } From 43bdc008a1c8e850332a83232a947c632656b2d5 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Fri, 8 Aug 2025 11:57:03 -0400 Subject: [PATCH 5/9] Eh. --- src/bin/evinfo.rs | 51 ++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/bin/evinfo.rs b/src/bin/evinfo.rs index da52482..4acd9ab 100644 --- a/src/bin/evinfo.rs +++ b/src/bin/evinfo.rs @@ -14,15 +14,15 @@ struct Args { fn main() { let args = Args::parse(); let devices = evdev::raw_stream::enumerate(); - devices.for_each(|(path, dev)| { + + for (path, dev) in devices { if is_joystick_like(&dev) { print_device(path, dev, args.verbose) } - }); + } } -// Todo: can use a macro here... -const JOYSTICK_BUTTONS: Vec { +const JOYSTICK_BUTTONS: &[evdev::KeyCode] = &[ evdev::KeyCode::BTN_TRIGGER_HAPPY1, evdev::KeyCode::BTN_TRIGGER_HAPPY2, evdev::KeyCode::BTN_TRIGGER_HAPPY3, @@ -33,11 +33,21 @@ const JOYSTICK_BUTTONS: Vec { evdev::KeyCode::BTN_TRIGGER_HAPPY8, evdev::KeyCode::BTN_TRIGGER_HAPPY9, evdev::KeyCode::BTN_TRIGGER_HAPPY10, -} + evdev::KeyCode::BTN_TRIGGER_HAPPY11, +]; fn is_joystick_like(device: &RawDevice) -> bool { + if let Some(_) = device.supported_absolute_axes() { + return true; + } - if device.supported_absolute_axes().map_or(false, |axes| axes.contains()); + if let Some(keys) = device.supported_keys() { + for key in keys.iter() { + if JOYSTICK_BUTTONS.contains(&key) { + return true; + } + } + } return false; } @@ -45,23 +55,28 @@ fn is_joystick_like(device: &RawDevice) -> bool { fn print_device(path: PathBuf, device: RawDevice, verbose: u8) { println!( "{}: \"{}\"", - path.to_str().unwrap_or_default(), - device.name().unwrap_or_default() + path.to_str().unwrap_or("unknown_device_path"), + device.name().unwrap_or("unknown_device_name") ); if verbose > 0 { let input_id = device.input_id(); - println!("\tUUID:\t\t'{}'", device.unique_name().unwrap_or_default()); - println!("\tVendor:\t\t'{:x}'", input_id.vendor()); - println!("\tProduct:\t'{:x}'", input_id.product()); + println!("\tUUID:\t\t'{}'", device.unique_name().unwrap_or("n/a")); + println!("\tVendor:\t\t'0x{:x}'", input_id.vendor()); + println!("\tProduct:\t'0x{:x}'", input_id.product()); println!("\tVersion:\t'{}'", input_id.version()); } - // if verbose > 1 { - // let absInfo = device.get_absinfo()?; - // if absInfo.count() > 0 { - // println!("\tAxis Data:"); - // absInfo.for_each(|info| println!("\t\t{} {}")); - // } - // } + if verbose > 1 { + if let Ok(abs_info) = device.get_absinfo() { + if abs_info.count() > 0 { + println!("\tAxis Data:"); + abs_info.for_each(|info| println!("\t\t{} {}")); + } + } + } + + if verbose > 0 { + println!(); + } } From d9babf5dc0465ca5f13da1ee4e898c89483688f9 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Sat, 9 Aug 2025 16:33:46 +0000 Subject: [PATCH 6/9] Improve config yaml schema (#16) Leverages custom unmarshaling to be more declarative for our config specification. Reviewed-on: https://git.annabunches.net/anna/joyful/pulls/16 Co-authored-by: Anna Rose Wiggins Co-committed-by: Anna Rose Wiggins --- cmd/joyful/main.go | 28 +- internal/config/devices.go | 18 +- internal/config/make_rule_targets.go | 11 +- internal/config/make_rule_targets_test.go | 229 ++++++++-------- internal/config/make_rules.go | 43 +-- internal/config/schema.go | 264 ++++++++++++++----- internal/config/variables.go | 4 +- internal/mappingrules/rule_target_relaxis.go | 5 +- 8 files changed, 364 insertions(+), 238 deletions(-) diff --git a/cmd/joyful/main.go b/cmd/joyful/main.go index 17482bf..f6cf6de 100644 --- a/cmd/joyful/main.go +++ b/cmd/joyful/main.go @@ -28,8 +28,11 @@ func readConfig(configDir string) *config.ConfigParser { return parser } -func initVirtualBuffers(config *config.ConfigParser) (map[string]*virtualdevice.EventBuffer, map[*evdev.InputDevice]*virtualdevice.EventBuffer) { - vDevices := config.CreateVirtualDevices() +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.") } @@ -40,20 +43,11 @@ func initVirtualBuffers(config *config.ConfigParser) (map[string]*virtualdevice. vBuffersByName[name] = virtualdevice.NewEventBuffer(device) vBuffersByDevice[device] = vBuffersByName[name] } - return vBuffersByName, vBuffersByDevice -} - -// Extracts the evdev devices from a list of virtual buffers and returns them. -func getVirtualDevices(buffers map[string]*virtualdevice.EventBuffer) map[string]*evdev.InputDevice { - devices := make(map[string]*evdev.InputDevice) - for name, buffer := range buffers { - devices[name] = buffer.Device.(*evdev.InputDevice) - } - return devices + return vDevices, vBuffersByName, vBuffersByDevice } func initPhysicalDevices(config *config.ConfigParser) map[string]*evdev.InputDevice { - pDeviceMap := config.ConnectPhysicalDevices() + pDeviceMap := config.InitPhysicalDevices() if len(pDeviceMap) == 0 { logger.Log("Warning: no physical devices found in configuration. No rules will work.") } @@ -77,13 +71,13 @@ func main() { logger.LogIfError(err, "Failed to initialize TTS") // Initialize virtual devices with event buffers - vBuffersByName, vBuffersByDevice := initVirtualBuffers(config) + vDevicesByName, vBuffersByName, vBuffersByDevice := initVirtualBuffers(config) // Initialize physical devices pDevices := initPhysicalDevices(config) // Load the rules - rules, eventChannel, cancel, wg := loadRules(config, pDevices, getVirtualDevices(vBuffersByName)) + rules, eventChannel, cancel, wg := loadRules(config, pDevices, vDevicesByName) // initialize the mode variable mode := config.GetModes()[0] @@ -139,7 +133,7 @@ func main() { wg.Wait() fmt.Println("Listeners exited. Parsing config.") config := readConfig(configDir) // reload the config - rules, eventChannel, cancel, wg = loadRules(config, pDevices, getVirtualDevices(vBuffersByName)) + rules, eventChannel, cancel, wg = loadRules(config, pDevices, vDevicesByName) fmt.Println("Config re-loaded. Only rule changes applied. Device and Mode changes require restart.") } @@ -159,7 +153,7 @@ func loadRules( ctx, cancel := context.WithCancel(context.Background()) // Initialize rules - rules := config.BuildRules(pDevices, vDevices) + rules := config.InitRules(pDevices, vDevices) logger.Logf("Created %d mapping rules.", len(rules)) // start listening for events on devices and timers diff --git a/internal/config/devices.go b/internal/config/devices.go index 9802bff..d933ed7 100644 --- a/internal/config/devices.go +++ b/internal/config/devices.go @@ -8,13 +8,13 @@ import ( "github.com/holoplot/go-evdev" ) -// CreateVirtualDevices will register any configured devices with type = virtual +// InitVirtualDevices 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 assumes Parse() has been called. // -// 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 { +// 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 { @@ -22,6 +22,8 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice continue } + deviceConfig := deviceConfig.Config.(DeviceConfigVirtual) + name := fmt.Sprintf("joyful-%s", deviceConfig.Name) var capabilities map[evdev.EvType][]evdev.EvCode @@ -74,13 +76,13 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice return deviceMap } -// ConnectPhysicalDevices will create InputDevices corresponding to any registered +// InitPhysicalDevices will create InputDevices corresponding to any registered // devices with type = physical. // -// This function assumes you have already called Parse() on the config directory. +// This function assumes Parse() has been called. // // This function should only be called once. -func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevice { +func (parser *ConfigParser) InitPhysicalDevices() map[string]*evdev.InputDevice { deviceMap := make(map[string]*evdev.InputDevice) for _, deviceConfig := range parser.config.Devices { @@ -88,6 +90,8 @@ func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevi continue } + deviceConfig := deviceConfig.Config.(DeviceConfigPhysical) + var infoName string var device *evdev.InputDevice var err error diff --git a/internal/config/make_rule_targets.go b/internal/config/make_rule_targets.go index 7e8c2eb..203a015 100644 --- a/internal/config/make_rule_targets.go +++ b/internal/config/make_rule_targets.go @@ -8,7 +8,7 @@ import ( "github.com/holoplot/go-evdev" ) -func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]Device) (*mappingrules.RuleTargetButton, error) { +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) @@ -27,7 +27,7 @@ func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]Device) ) } -func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]Device) (*mappingrules.RuleTargetAxis, error) { +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) @@ -57,7 +57,7 @@ func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]Device) ( ) } -func makeRuleTargetRelaxis(targetConfig RuleTargetConfig, devs map[string]Device) (*mappingrules.RuleTargetRelaxis, error) { +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) @@ -72,11 +72,10 @@ func makeRuleTargetRelaxis(targetConfig RuleTargetConfig, devs map[string]Device targetConfig.Device, device, eventCode, - targetConfig.Inverted, ) } -func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, allModes []string) (*mappingrules.RuleTargetModeSelect, error) { +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") } @@ -92,7 +91,7 @@ func hasError(_ any, err error) bool { // calculateDeadzones produces the deadzone start and end values in absolute terms // TODO: on the one hand, this logic feels betten encapsulated in mappingrules. On the other hand, // passing even more parameters to NewRuleTargetAxis feels terrible -func calculateDeadzones(targetConfig RuleTargetConfig, device Device, axis evdev.EvCode) (int32, int32, error) { +func calculateDeadzones(targetConfig RuleTargetConfigAxis, device Device, axis evdev.EvCode) (int32, int32, error) { var deadzoneStart, deadzoneEnd int32 deadzoneStart = 0 diff --git a/internal/config/make_rule_targets_test.go b/internal/config/make_rule_targets_test.go index 6e71fa6..7ee8fb8 100644 --- a/internal/config/make_rule_targets_test.go +++ b/internal/config/make_rule_targets_test.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "testing" "github.com/holoplot/go-evdev" @@ -12,7 +13,6 @@ type MakeRuleTargetsTests struct { suite.Suite devs map[string]Device deviceMock *DeviceMock - config RuleTargetConfig } type DeviceMock struct { @@ -47,198 +47,197 @@ func (t *MakeRuleTargetsTests) SetupSuite() { } } -func (t *MakeRuleTargetsTests) SetupSubTest() { - t.config = RuleTargetConfig{ - Device: "test", - } -} - func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() { + config := RuleTargetConfigButton{Device: "test"} + t.Run("Standard keycode", func() { - t.config.Button = "BTN_TRIGGER" - rule, err := makeRuleTargetButton(t.config, t.devs) + config.Button = "BTN_TRIGGER" + rule, err := makeRuleTargetButton(config, t.devs) t.Nil(err) t.EqualValues(evdev.BTN_TRIGGER, rule.Button) }) t.Run("Hex code", func() { - t.config.Button = "0x2fd" - rule, err := makeRuleTargetButton(t.config, t.devs) + config.Button = "0x2fd" + rule, err := makeRuleTargetButton(config, t.devs) t.Nil(err) t.EqualValues(evdev.EvCode(0x2fd), rule.Button) }) t.Run("Index", func() { - t.config.Button = "3" - rule, err := makeRuleTargetButton(t.config, t.devs) + config.Button = "3" + rule, err := makeRuleTargetButton(config, t.devs) t.Nil(err) t.EqualValues(evdev.BTN_TOP, rule.Button) }) t.Run("Index too high", func() { - t.config.Button = "74" - _, err := makeRuleTargetButton(t.config, t.devs) + config.Button = "74" + _, err := makeRuleTargetButton(config, t.devs) t.NotNil(err) }) t.Run("Un-prefixed keycode", func() { - t.config.Button = "pinkie" - rule, err := makeRuleTargetButton(t.config, t.devs) + config.Button = "pinkie" + rule, err := makeRuleTargetButton(config, t.devs) t.Nil(err) t.EqualValues(evdev.BTN_PINKIE, rule.Button) }) t.Run("Invalid keycode", func() { - t.config.Button = "foo" - _, err := makeRuleTargetButton(t.config, t.devs) + config.Button = "foo" + _, err := makeRuleTargetButton(config, t.devs) t.NotNil(err) }) } func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { - t.Run("Standard code", func() { - t.config.Axis = "ABS_X" - rule, err := makeRuleTargetAxis(t.config, t.devs) - t.Nil(err) - t.EqualValues(evdev.ABS_X, rule.Axis) - }) + codeTestCases := []struct { + input string + output evdev.EvCode + }{ + {"ABS_X", evdev.ABS_X}, + {"0x01", evdev.ABS_Y}, + {"x", evdev.ABS_X}, + } - t.Run("Hex code", func() { - t.config.Axis = "0x01" - rule, err := makeRuleTargetAxis(t.config, t.devs) - t.Nil(err) - t.EqualValues(evdev.ABS_Y, rule.Axis) - }) + for _, tc := range codeTestCases { + t.Run(fmt.Sprintf("KeyCode %s", tc.input), func() { + config := RuleTargetConfigAxis{Device: "test"} + config.Axis = tc.input + rule, err := makeRuleTargetAxis(config, t.devs) + t.Nil(err) + t.EqualValues(tc.output, rule.Axis) - t.Run("Un-prefixed code", func() { - t.config.Axis = "x" - rule, err := makeRuleTargetAxis(t.config, t.devs) - t.Nil(err) - t.EqualValues(evdev.ABS_X, rule.Axis) - }) + }) + } t.Run("Invalid code", func() { - t.config.Axis = "foo" - _, err := makeRuleTargetAxis(t.config, t.devs) + config := RuleTargetConfigAxis{Device: "test"} + config.Axis = "foo" + _, err := makeRuleTargetAxis(config, t.devs) t.NotNil(err) }) t.Run("Invalid deadzone", func() { - t.config.Axis = "x" - t.config.DeadzoneEnd = 100 - t.config.DeadzoneStart = 1000 - _, err := makeRuleTargetAxis(t.config, t.devs) + config := RuleTargetConfigAxis{Device: "test"} + config.Axis = "x" + config.DeadzoneEnd = 100 + config.DeadzoneStart = 1000 + _, err := makeRuleTargetAxis(config, t.devs) t.NotNil(err) }) - t.Run("Deadzone center/size", func() { - t.config.Axis = "x" - t.config.DeadzoneCenter = 5000 - t.config.DeadzoneSize = 1000 - rule, err := makeRuleTargetAxis(t.config, t.devs) - t.Nil(err) - t.EqualValues(4500, rule.DeadzoneStart) - t.EqualValues(5500, rule.DeadzoneEnd) - }) + relDeadzoneTestCases := []struct { + inCenter int32 + inSize int32 + outStart int32 + outEnd int32 + }{ + {5000, 1000, 4500, 5500}, + {0, 500, 0, 500}, + {10000, 500, 9500, 10000}, + } - t.Run("Deadzone center/size lower boundary", func() { - t.config.Axis = "x" - t.config.DeadzoneCenter = 0 - t.config.DeadzoneSize = 500 - rule, err := makeRuleTargetAxis(t.config, t.devs) - t.Nil(err) - t.EqualValues(0, rule.DeadzoneStart) - t.EqualValues(500, rule.DeadzoneEnd) - }) + for _, tc := range relDeadzoneTestCases { + t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() { + config := RuleTargetConfigAxis{ + Device: "test", + Axis: "x", + DeadzoneCenter: tc.inCenter, + DeadzoneSize: tc.inSize, + } + rule, err := makeRuleTargetAxis(config, t.devs) - t.Run("Deadzone center/size upper boundary", func() { - t.config.Axis = "x" - t.config.DeadzoneCenter = 10000 - t.config.DeadzoneSize = 500 - rule, err := makeRuleTargetAxis(t.config, t.devs) - t.Nil(err) - t.EqualValues(9500, rule.DeadzoneStart) - t.EqualValues(10000, rule.DeadzoneEnd) - }) + t.Nil(err) + t.Equal(tc.outStart, rule.DeadzoneStart) + t.Equal(tc.outEnd, rule.DeadzoneEnd) + }) + } t.Run("Deadzone center/size invalid center", func() { - t.config.Axis = "x" - t.config.DeadzoneCenter = 20000 - t.config.DeadzoneSize = 500 - _, err := makeRuleTargetAxis(t.config, t.devs) + config := RuleTargetConfigAxis{ + Device: "test", + Axis: "x", + DeadzoneCenter: 20000, + DeadzoneSize: 500, + } + _, err := makeRuleTargetAxis(config, t.devs) t.NotNil(err) }) - t.Run("Deadzone center/percent", func() { - t.config.Axis = "x" - t.config.DeadzoneCenter = 5000 - t.config.DeadzoneSizePercent = 10 - rule, err := makeRuleTargetAxis(t.config, t.devs) - t.Nil(err) - t.EqualValues(4500, rule.DeadzoneStart) - t.EqualValues(5500, rule.DeadzoneEnd) - }) + relDeadzonePercentTestCases := []struct { + inCenter int32 + inSizePercent int32 + outStart int32 + outEnd int32 + }{ + {5000, 10, 4500, 5500}, + {0, 10, 0, 1000}, + {10000, 10, 9000, 10000}, + } - t.Run("Deadzone center/percent lower boundary", func() { - t.config.Axis = "x" - t.config.DeadzoneCenter = 0 - t.config.DeadzoneSizePercent = 10 - rule, err := makeRuleTargetAxis(t.config, t.devs) - t.Nil(err) - t.EqualValues(0, rule.DeadzoneStart) - t.EqualValues(1000, rule.DeadzoneEnd) - }) + for _, tc := range relDeadzonePercentTestCases { + t.Run(fmt.Sprintf("Relative percent deadzone %d +- %d%%", tc.inCenter, tc.inSizePercent), func() { + config := RuleTargetConfigAxis{ + Device: "test", + Axis: "x", + DeadzoneCenter: tc.inCenter, + DeadzoneSizePercent: tc.inSizePercent, + } + rule, err := makeRuleTargetAxis(config, t.devs) - t.Run("Deadzone center/percent upper boundary", func() { - t.config.Axis = "x" - t.config.DeadzoneCenter = 10000 - t.config.DeadzoneSizePercent = 10 - rule, err := makeRuleTargetAxis(t.config, t.devs) - t.Nil(err) - t.EqualValues(9000, rule.DeadzoneStart) - t.EqualValues(10000, rule.DeadzoneEnd) - }) + t.Nil(err) + t.Equal(tc.outStart, rule.DeadzoneStart) + t.Equal(tc.outEnd, rule.DeadzoneEnd) + }) + } t.Run("Deadzone center/percent invalid center", func() { - t.config.Axis = "x" - t.config.DeadzoneCenter = 20000 - t.config.DeadzoneSizePercent = 10 - _, err := makeRuleTargetAxis(t.config, t.devs) + config := RuleTargetConfigAxis{ + Device: "test", + Axis: "x", + DeadzoneCenter: 20000, + DeadzoneSizePercent: 10, + } + _, err := makeRuleTargetAxis(config, t.devs) t.NotNil(err) }) } func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() { + config := RuleTargetConfigRelaxis{Device: "test"} + t.Run("Standard keycode", func() { - t.config.Axis = "REL_WHEEL" - rule, err := makeRuleTargetRelaxis(t.config, t.devs) + config.Axis = "REL_WHEEL" + rule, err := makeRuleTargetRelaxis(config, t.devs) t.Nil(err) t.EqualValues(evdev.REL_WHEEL, rule.Axis) }) t.Run("Hex keycode", func() { - t.config.Axis = "0x00" - rule, err := makeRuleTargetRelaxis(t.config, t.devs) + config.Axis = "0x00" + rule, err := makeRuleTargetRelaxis(config, t.devs) t.Nil(err) t.EqualValues(evdev.REL_X, rule.Axis) }) t.Run("Un-prefixed keycode", func() { - t.config.Axis = "wheel" - rule, err := makeRuleTargetRelaxis(t.config, t.devs) + config.Axis = "wheel" + rule, err := makeRuleTargetRelaxis(config, t.devs) t.Nil(err) t.EqualValues(evdev.REL_WHEEL, rule.Axis) }) t.Run("Invalid keycode", func() { - t.config.Axis = "foo" - _, err := makeRuleTargetRelaxis(t.config, t.devs) + config.Axis = "foo" + _, err := makeRuleTargetRelaxis(config, t.devs) t.NotNil(err) }) t.Run("Incorrect axis type", func() { - t.config.Axis = "ABS_X" - _, err := makeRuleTargetRelaxis(t.config, t.devs) + config.Axis = "ABS_X" + _, err := makeRuleTargetRelaxis(config, t.devs) t.NotNil(err) }) } diff --git a/internal/config/make_rules.go b/internal/config/make_rules.go index 647987c..9baf9d7 100644 --- a/internal/config/make_rules.go +++ b/internal/config/make_rules.go @@ -14,7 +14,7 @@ import ( // 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[][]rule // For very large rule-bases this may be helpful for staying performant. -func (parser *ConfigParser) BuildRules(pInputDevs map[string]*evdev.InputDevice, vInputDevs map[string]*evdev.InputDevice) []mappingrules.MappingRule { +func (parser *ConfigParser) InitRules(pInputDevs map[string]*evdev.InputDevice, vInputDevs map[string]*evdev.InputDevice) []mappingrules.MappingRule { rules := make([]mappingrules.MappingRule, 0) modes := parser.GetModes() @@ -42,21 +42,21 @@ func (parser *ConfigParser) BuildRules(pInputDevs map[string]*evdev.InputDevice, switch strings.ToLower(ruleConfig.Type) { case RuleTypeButton: - newRule, err = makeMappingRuleButton(ruleConfig, pDevs, vDevs, base) + newRule, err = makeMappingRuleButton(ruleConfig.Config.(RuleConfigButton), pDevs, vDevs, base) case RuleTypeButtonCombo: - newRule, err = makeMappingRuleCombo(ruleConfig, pDevs, vDevs, base) - case RuleTypeLatched: - newRule, err = makeMappingRuleLatched(ruleConfig, pDevs, vDevs, base) + 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, pDevs, vDevs, base) + newRule, err = makeMappingRuleAxis(ruleConfig.Config.(RuleConfigAxis), pDevs, vDevs, base) case RuleTypeAxisCombined: - newRule, err = makeMappingRuleAxisCombined(ruleConfig, pDevs, vDevs, base) + newRule, err = makeMappingRuleAxisCombined(ruleConfig.Config.(RuleConfigAxisCombined), pDevs, vDevs, base) case RuleTypeAxisToButton: - newRule, err = makeMappingRuleAxisToButton(ruleConfig, pDevs, vDevs, base) + newRule, err = makeMappingRuleAxisToButton(ruleConfig.Config.(RuleConfigAxisToButton), pDevs, vDevs, base) case RuleTypeAxisToRelaxis: - newRule, err = makeMappingRuleAxisToRelaxis(ruleConfig, pDevs, vDevs, base) + newRule, err = makeMappingRuleAxisToRelaxis(ruleConfig.Config.(RuleConfigAxisToRelaxis), pDevs, vDevs, base) case RuleTypeModeSelect: - newRule, err = makeMappingRuleModeSelect(ruleConfig, pDevs, modes, base) + newRule, err = makeMappingRuleModeSelect(ruleConfig.Config.(RuleConfigModeSelect), pDevs, modes, base) default: err = fmt.Errorf("bad rule type '%s' for rule '%s'", ruleConfig.Type, ruleConfig.Name) } @@ -72,7 +72,14 @@ func (parser *ConfigParser) BuildRules(pInputDevs map[string]*evdev.InputDevice, return rules } -func makeMappingRuleButton(ruleConfig RuleConfig, +// 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) { @@ -90,7 +97,7 @@ func makeMappingRuleButton(ruleConfig RuleConfig, return mappingrules.NewMappingRuleButton(base, input, output), nil } -func makeMappingRuleCombo(ruleConfig RuleConfig, +func makeMappingRuleCombo(ruleConfig RuleConfigButtonCombo, pDevs map[string]Device, vDevs map[string]Device, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) { @@ -112,7 +119,7 @@ func makeMappingRuleCombo(ruleConfig RuleConfig, return mappingrules.NewMappingRuleButtonCombo(base, inputs, output), nil } -func makeMappingRuleLatched(ruleConfig RuleConfig, +func makeMappingRuleLatched(ruleConfig RuleConfigButtonLatched, pDevs map[string]Device, vDevs map[string]Device, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) { @@ -130,7 +137,7 @@ func makeMappingRuleLatched(ruleConfig RuleConfig, return mappingrules.NewMappingRuleButtonLatched(base, input, output), nil } -func makeMappingRuleAxis(ruleConfig RuleConfig, +func makeMappingRuleAxis(ruleConfig RuleConfigAxis, pDevs map[string]Device, vDevs map[string]Device, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) { @@ -148,7 +155,7 @@ func makeMappingRuleAxis(ruleConfig RuleConfig, return mappingrules.NewMappingRuleAxis(base, input, output), nil } -func makeMappingRuleAxisCombined(ruleConfig RuleConfig, +func makeMappingRuleAxisCombined(ruleConfig RuleConfigAxisCombined, pDevs map[string]Device, vDevs map[string]Device, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisCombined, error) { @@ -171,7 +178,7 @@ func makeMappingRuleAxisCombined(ruleConfig RuleConfig, return mappingrules.NewMappingRuleAxisCombined(base, inputLower, inputUpper, output), nil } -func makeMappingRuleAxisToButton(ruleConfig RuleConfig, +func makeMappingRuleAxisToButton(ruleConfig RuleConfigAxisToButton, pDevs map[string]Device, vDevs map[string]Device, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) { @@ -189,7 +196,7 @@ func makeMappingRuleAxisToButton(ruleConfig RuleConfig, return mappingrules.NewMappingRuleAxisToButton(base, input, output, ruleConfig.RepeatRateMin, ruleConfig.RepeatRateMax), nil } -func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfig, +func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfigAxisToRelaxis, pDevs map[string]Device, vDevs map[string]Device, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToRelaxis, error) { @@ -211,7 +218,7 @@ func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfig, ruleConfig.Increment), nil } -func makeMappingRuleModeSelect(ruleConfig RuleConfig, +func makeMappingRuleModeSelect(ruleConfig RuleConfigModeSelect, pDevs map[string]Device, modes []string, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) { diff --git a/internal/config/schema.go b/internal/config/schema.go index 1ea3527..ad91f28 100644 --- a/internal/config/schema.go +++ b/internal/config/schema.go @@ -1,79 +1,213 @@ // These types comprise the YAML schema for configuring Joyful. // The config files will be combined and then unmarshalled into this -// -// TODO: currently the types in here aren't especially strong; each one is -// decomposed into a different object based on the Type fields. We should implement -// some sort of delayed unmarshalling technique, for example see ideas at -// https://stackoverflow.com/questions/70635636/unmarshaling-yaml-into-different-struct-based-off-yaml-field -// Then we can be more explicit about the interface here. package config +import ( + "fmt" +) + type Config struct { - Devices []DeviceConfig `yaml:"devices"` - Modes []string `yaml:"modes,omitempty"` - Rules []RuleConfig `yaml:"rules"` + Devices []DeviceConfig + Modes []string + Rules []RuleConfig } +// These top-level structs use custom unmarshaling to unpack each available sub-type type DeviceConfig struct { - Name string `yaml:"name"` - Type string `yaml:"type"` - DeviceName string `yaml:"device_name,omitempty"` - DevicePath string `yaml:"device_path,omitempty"` - Preset string `yaml:"preset,omitempty"` - NumButtons int `yaml:"num_buttons,omitempty"` - NumAxes int `yaml:"num_axes,omitempty"` - NumRelativeAxes int `yaml:"num_rel_axes"` - Buttons []string `yaml:"buttons,omitempty"` - Axes []string `yaml:"axes,omitempty"` - RelativeAxes []string `yaml:"rel_axes,omitempty"` - Lock bool `yaml:"lock,omitempty"` + Type string + Config interface{} } type RuleConfig struct { - Name string `yaml:"name,omitempty"` - Type string `yaml:"type"` - Input RuleTargetConfig `yaml:"input,omitempty"` - InputLower RuleTargetConfig `yaml:"input_lower,omitempty"` - InputUpper RuleTargetConfig `yaml:"input_upper,omitempty"` - Inputs []RuleTargetConfig `yaml:"inputs,omitempty"` - Output RuleTargetConfig `yaml:"output"` - Modes []string `yaml:"modes,omitempty"` - RepeatRateMin int `yaml:"repeat_rate_min,omitempty"` - RepeatRateMax int `yaml:"repeat_rate_max,omitempty"` - Increment int `yaml:"increment,omitempty"` + Type string + Name string + Modes []string + Config interface{} } -type RuleTargetConfig struct { - Device string `yaml:"device,omitempty"` - Button string `yaml:"button,omitempty"` - Axis string `yaml:"axis,omitempty"` - DeadzoneCenter int32 `yaml:"deadzone_center,omitempty"` - DeadzoneSize int32 `yaml:"deadzone_size,omitempty"` - DeadzoneSizePercent int32 `yaml:"deadzone_size_percent,omitempty"` - DeadzoneStart int32 `yaml:"deadzone_start,omitempty"` - DeadzoneEnd int32 `yaml:"deadzone_end,omitempty"` - Inverted bool `yaml:"inverted,omitempty"` - Modes []string `yaml:"modes,omitempty"` +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 *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error { +func (dc *DeviceConfigPhysical) UnmarshalYAML(unmarshal func(data interface{}) error) error { var raw struct { - Name string - Type string - DeviceName string `yaml:"device_name"` - DevicePath string `yaml:"device_path"` - Preset string - NumButtons int `yaml:"num_buttons"` - NumAxes int `yaml:"num_axes"` - NumRelativeAxes int `yaml:"num_rel_axes"` - Buttons []string - Axes []string - RelativeAxes []string `yaml:"relative_axes"` - Lock bool `yaml:"lock,omitempty"` + 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) @@ -81,19 +215,11 @@ func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) er return err } - *dc = DeviceConfig{ - Name: raw.Name, - Type: raw.Type, - DeviceName: raw.DeviceName, - DevicePath: raw.DevicePath, - Preset: raw.Preset, - NumButtons: raw.NumButtons, - NumAxes: raw.NumAxes, - NumRelativeAxes: raw.NumRelativeAxes, - Buttons: raw.Buttons, - Axes: raw.Axes, - RelativeAxes: raw.RelativeAxes, - Lock: raw.Lock, + *dc = DeviceConfigPhysical{ + Name: raw.Name, + DeviceName: raw.DeviceName, + DevicePath: raw.DevicePath, + Lock: raw.Lock, } return nil } diff --git a/internal/config/variables.go b/internal/config/variables.go index e4e0bf0..6e62977 100644 --- a/internal/config/variables.go +++ b/internal/config/variables.go @@ -15,12 +15,12 @@ const ( RuleTypeButton = "button" RuleTypeButtonCombo = "button-combo" - RuleTypeLatched = "button-latched" + RuleTypeButtonLatched = "button-latched" RuleTypeAxis = "axis" RuleTypeAxisCombined = "axis-combined" - RuleTypeModeSelect = "mode-select" RuleTypeAxisToButton = "axis-to-button" RuleTypeAxisToRelaxis = "axis-to-relaxis" + RuleTypeModeSelect = "mode-select" CodePrefixButton = "BTN" CodePrefixKey = "KEY" diff --git a/internal/mappingrules/rule_target_relaxis.go b/internal/mappingrules/rule_target_relaxis.go index 8de8c0b..1942c4b 100644 --- a/internal/mappingrules/rule_target_relaxis.go +++ b/internal/mappingrules/rule_target_relaxis.go @@ -8,19 +8,16 @@ type RuleTargetRelaxis struct { DeviceName string Device Device Axis evdev.EvCode - Inverted bool } func NewRuleTargetRelaxis(device_name string, device Device, - axis evdev.EvCode, - inverted bool) (*RuleTargetRelaxis, error) { + axis evdev.EvCode) (*RuleTargetRelaxis, error) { return &RuleTargetRelaxis{ DeviceName: device_name, Device: device, Axis: axis, - Inverted: inverted, }, nil } From dde97be4a01be41cfe89f6ce64608481df375f78 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Mon, 11 Aug 2025 11:55:16 -0400 Subject: [PATCH 7/9] Fix static list (still only partially filled out) --- src/bin/evinfo.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bin/evinfo.rs b/src/bin/evinfo.rs index 4acd9ab..a9d7648 100644 --- a/src/bin/evinfo.rs +++ b/src/bin/evinfo.rs @@ -22,7 +22,7 @@ fn main() { } } -const JOYSTICK_BUTTONS: &[evdev::KeyCode] = &[ +static JOYSTICK_BUTTONS: [evdev::KeyCode; 11] = [ evdev::KeyCode::BTN_TRIGGER_HAPPY1, evdev::KeyCode::BTN_TRIGGER_HAPPY2, evdev::KeyCode::BTN_TRIGGER_HAPPY3, @@ -71,7 +71,9 @@ fn print_device(path: PathBuf, device: RawDevice, verbose: u8) { if let Ok(abs_info) = device.get_absinfo() { if abs_info.count() > 0 { println!("\tAxis Data:"); - abs_info.for_each(|info| println!("\t\t{} {}")); + for (axis, info) in abs_info { + println!("\t\t{} {}-{}", axis, info.minimum(), info.maximum()); + } } } } From 8d2b15a7c8223af91dc190aa3aa9c8a5e7fe494a Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Tue, 12 Aug 2025 00:57:11 +0000 Subject: [PATCH 8/9] Move initialization code closer to the appropriate structs. (#17) Reviewed-on: https://git.annabunches.net/anna/joyful/pulls/17 Co-authored-by: Anna Rose Wiggins Co-committed-by: Anna Rose Wiggins --- cmd/evinfo/main.go | 5 +- cmd/joyful/config.go | 147 +++++++++++ cmd/joyful/main.go | 105 ++------ internal/config/configparser.go | 77 ------ internal/config/devices.go | 221 ---------------- internal/config/interfaces.go | 7 - internal/config/make_rule_targets.go | 145 ----------- internal/config/make_rules.go | 237 ------------------ internal/config/modes.go | 19 -- internal/configparser/configparser.go | 67 +++++ internal/{config => configparser}/schema.go | 2 +- internal/configparser/variables.go | 15 ++ internal/{config => eventcodes}/codes.go | 13 +- internal/{config => eventcodes}/codes_test.go | 8 +- internal/eventcodes/variables.go | 90 +++++++ .../init_rule_targets_test.go} | 59 ++--- internal/mappingrules/init_rules.go | 79 ++++++ internal/mappingrules/mapping_rule_axis.go | 23 +- .../mapping_rule_axis_combined.go | 24 +- .../mapping_rule_axis_combined_test.go | 17 +- .../mapping_rule_axis_to_button.go | 25 +- .../mapping_rule_axis_to_button_test.go | 77 +++--- .../mapping_rule_axis_to_relaxis.go | 28 ++- internal/mappingrules/mapping_rule_button.go | 25 +- .../mappingrules/mapping_rule_button_combo.go | 29 ++- .../mapping_rule_button_latched.go | 25 +- .../mappingrules/mapping_rule_button_test.go | 12 +- .../mappingrules/mapping_rule_mode_select.go | 26 +- internal/mappingrules/math.go | 13 + internal/mappingrules/rule_target_axis.go | 73 ++++++ internal/mappingrules/rule_target_button.go | 27 +- .../mappingrules/rule_target_modeselect.go | 9 + internal/mappingrules/rule_target_relaxis.go | 26 +- internal/mappingrules/variables.go | 12 + internal/virtualdevice/cleanup.go | 35 --- internal/virtualdevice/eventbuffer.go | 8 +- internal/virtualdevice/eventbuffer_test.go | 105 ++++---- internal/virtualdevice/init.go | 165 ++++++++++++ .../init_test.go} | 14 +- .../{config => virtualdevice}/variables.go | 102 +------- 40 files changed, 1087 insertions(+), 1109 deletions(-) create mode 100644 cmd/joyful/config.go delete mode 100644 internal/config/configparser.go delete mode 100644 internal/config/devices.go delete mode 100644 internal/config/interfaces.go delete mode 100644 internal/config/make_rule_targets.go delete mode 100644 internal/config/make_rules.go delete mode 100644 internal/config/modes.go create mode 100644 internal/configparser/configparser.go rename internal/{config => configparser}/schema.go (99%) create mode 100644 internal/configparser/variables.go rename internal/{config => eventcodes}/codes.go (81%) rename internal/{config => eventcodes}/codes_test.go (94%) create mode 100644 internal/eventcodes/variables.go rename internal/{config/make_rule_targets_test.go => mappingrules/init_rule_targets_test.go} (71%) create mode 100644 internal/mappingrules/init_rules.go create mode 100644 internal/mappingrules/variables.go delete mode 100644 internal/virtualdevice/cleanup.go create mode 100644 internal/virtualdevice/init.go rename internal/{config/devices_test.go => virtualdevice/init_test.go} (91%) rename internal/{config => virtualdevice}/variables.go (71%) diff --git a/cmd/evinfo/main.go b/cmd/evinfo/main.go index c2cc8f0..12a0ecb 100644 --- a/cmd/evinfo/main.go +++ b/cmd/evinfo/main.go @@ -5,7 +5,8 @@ import ( "slices" // TODO: using config here feels like bad coupling... ButtonFromIndex might need a refactor / move - "git.annabunches.net/annabunches/joyful/internal/config" + + "git.annabunches.net/annabunches/joyful/internal/eventcodes" "git.annabunches.net/annabunches/joyful/internal/logger" "github.com/holoplot/go-evdev" flag "github.com/spf13/pflag" @@ -20,7 +21,7 @@ func isJoystickLike(device *evdev.InputDevice) bool { if slices.Contains(types, evdev.EV_KEY) { buttons := device.CapableEvents(evdev.EV_KEY) - for _, code := range config.ButtonFromIndex { + for _, code := range eventcodes.ButtonFromIndex { if slices.Contains(buttons, code) { return true } diff --git a/cmd/joyful/config.go b/cmd/joyful/config.go new file mode 100644 index 0000000..2b43380 --- /dev/null +++ b/cmd/joyful/config.go @@ -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[][]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 +} diff --git a/cmd/joyful/main.go b/cmd/joyful/main.go index f6cf6de..bcdeccc 100644 --- a/cmd/joyful/main.go +++ b/cmd/joyful/main.go @@ -1,19 +1,15 @@ package main import ( - "context" "fmt" "os" "strings" - "sync" "github.com/holoplot/go-evdev" flag "github.com/spf13/pflag" - "git.annabunches.net/annabunches/joyful/internal/config" + "git.annabunches.net/annabunches/joyful/internal/configparser" "git.annabunches.net/annabunches/joyful/internal/logger" - "git.annabunches.net/annabunches/joyful/internal/mappingrules" - "git.annabunches.net/annabunches/joyful/internal/virtualdevice" ) func getConfigDir(dir string) string { @@ -21,39 +17,6 @@ func getConfigDir(dir string) string { return os.ExpandEnv(configDir) } -func readConfig(configDir string) *config.ConfigParser { - parser := &config.ConfigParser{} - err := parser.Parse(configDir) - logger.FatalIfError(err, "Failed to parse config") - return parser -} - -func initVirtualBuffers(config *config.ConfigParser) (map[string]*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() { // parse command-line var configFlag string @@ -64,7 +27,8 @@ func main() { // parse configs configDir := getConfigDir(configFlag) - config := readConfig(configDir) + config, err := configparser.ParseConfig(configDir) + logger.FatalIfError(err, "Failed to parse configuration") // initialize TTS tts, err := newTTS(ttsOps) @@ -76,20 +40,26 @@ func main() { // Initialize physical devices pDevices := initPhysicalDevices(config) - // Load the rules - rules, eventChannel, cancel, wg := loadRules(config, pDevices, vDevicesByName) + // initialize the mode variables + var mode string + modes := config.Modes + if len(modes) == 0 { + mode = "*" + } else { + mode = config.Modes[0] + } - // initialize the mode variable - mode := config.GetModes()[0] + // Load the rules + rules, eventChannel, cancel, wg := loadRules(config, pDevices, vDevicesByName, modes) // initialize TTS phrases for modes - for _, m := range config.GetModes() { + for _, m := range modes { tts.AddMessage(m) logger.LogDebugf("Added TTS message '%s'", m) } fmt.Println("Joyful Running! Press Ctrl+C to quit. Press Enter to reload rules.") - if len(config.GetModes()) > 1 { + if len(modes) > 0 { logger.Logf("Initial mode set to '%s'", mode) } @@ -127,13 +97,18 @@ func main() { case ChannelEventReload: // stop existing channels + config, err := configparser.ParseConfig(configDir) // reload the config + if err != nil { + logger.LogError(err, "Failed to parse config, no changes made") + continue + } + fmt.Println("Reloading rules.") cancel() fmt.Println("Waiting for existing listeners to exit. Provide input from each of your devices.") wg.Wait() - fmt.Println("Listeners exited. Parsing config.") - config := readConfig(configDir) // reload the config - rules, eventChannel, cancel, wg = loadRules(config, pDevices, vDevicesByName) + fmt.Println("Listeners exited. Loading new rules.") + rules, eventChannel, cancel, wg = loadRules(config, pDevices, vDevicesByName, modes) fmt.Println("Config re-loaded. Only rule changes applied. Device and Mode changes require restart.") } @@ -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 -} diff --git a/internal/config/configparser.go b/internal/config/configparser.go deleted file mode 100644 index 564c00d..0000000 --- a/internal/config/configparser.go +++ /dev/null @@ -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() -// 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 -} diff --git a/internal/config/devices.go b/internal/config/devices.go deleted file mode 100644 index d933ed7..0000000 --- a/internal/config/devices.go +++ /dev/null @@ -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 -} diff --git a/internal/config/interfaces.go b/internal/config/interfaces.go deleted file mode 100644 index 0b9fa42..0000000 --- a/internal/config/interfaces.go +++ /dev/null @@ -1,7 +0,0 @@ -package config - -import "github.com/holoplot/go-evdev" - -type Device interface { - AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error) -} diff --git a/internal/config/make_rule_targets.go b/internal/config/make_rule_targets.go deleted file mode 100644 index 203a015..0000000 --- a/internal/config/make_rule_targets.go +++ /dev/null @@ -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 -} diff --git a/internal/config/make_rules.go b/internal/config/make_rules.go deleted file mode 100644 index 9baf9d7..0000000 --- a/internal/config/make_rules.go +++ /dev/null @@ -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[][]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 -} diff --git a/internal/config/modes.go b/internal/config/modes.go deleted file mode 100644 index ad3dee2..0000000 --- a/internal/config/modes.go +++ /dev/null @@ -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 -} diff --git a/internal/configparser/configparser.go b/internal/configparser/configparser.go new file mode 100644 index 0000000..3daa217 --- /dev/null +++ b/internal/configparser/configparser.go @@ -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 +} diff --git a/internal/config/schema.go b/internal/configparser/schema.go similarity index 99% rename from internal/config/schema.go rename to internal/configparser/schema.go index ad91f28..8b70521 100644 --- a/internal/config/schema.go +++ b/internal/configparser/schema.go @@ -1,7 +1,7 @@ // These types comprise the YAML schema for configuring Joyful. // The config files will be combined and then unmarshalled into this -package config +package configparser import ( "fmt" diff --git a/internal/configparser/variables.go b/internal/configparser/variables.go new file mode 100644 index 0000000..77e2b9c --- /dev/null +++ b/internal/configparser/variables.go @@ -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" +) diff --git a/internal/config/codes.go b/internal/eventcodes/codes.go similarity index 81% rename from internal/config/codes.go rename to internal/eventcodes/codes.go index c879feb..a7515a8 100644 --- a/internal/config/codes.go +++ b/internal/eventcodes/codes.go @@ -1,4 +1,4 @@ -package config +package eventcodes import ( "fmt" @@ -8,17 +8,17 @@ import ( "github.com/holoplot/go-evdev" ) -func parseCodeButton(code string) (evdev.EvCode, error) { +func ParseCodeButton(code string) (evdev.EvCode, error) { prefix := CodePrefixButton if strings.HasPrefix(code, 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) var codeLookup map[string]evdev.EvCode @@ -70,3 +70,8 @@ func parseCode(code, prefix string) (evdev.EvCode, error) { return eventCode, nil } } + +// hasError exists solely to switch on errors in conditional and case statements +func hasError(_ any, err error) bool { + return err != nil +} diff --git a/internal/config/codes_test.go b/internal/eventcodes/codes_test.go similarity index 94% rename from internal/config/codes_test.go rename to internal/eventcodes/codes_test.go index 6e80291..4d72526 100644 --- a/internal/config/codes_test.go +++ b/internal/eventcodes/codes_test.go @@ -1,4 +1,4 @@ -package config +package eventcodes import ( "fmt" @@ -18,7 +18,7 @@ func TestRunnerEventCodeParserTests(t *testing.T) { func parseCodeTestCase(t *EventCodeParserTests, in string, out evdev.EvCode, prefix string) { t.Run(fmt.Sprintf("%s: %s", prefix, in), func() { - code, err := parseCode(in, prefix) + code, err := ParseCode(in, prefix) t.Nil(err) t.EqualValues(out, code) }) @@ -38,7 +38,7 @@ func (t *EventCodeParserTests) TestParseCodeButton() { for _, testCase := range testCases { t.Run(testCase.in, func() { - code, err := parseCodeButton(testCase.in) + code, err := ParseCodeButton(testCase.in) t.Nil(err) t.EqualValues(code, testCase.out) }) @@ -134,7 +134,7 @@ func (t *EventCodeParserTests) TestParseCode() { for _, testCase := range testCases { 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) }) } diff --git a/internal/eventcodes/variables.go b/internal/eventcodes/variables.go new file mode 100644 index 0000000..d63b92d --- /dev/null +++ b/internal/eventcodes/variables.go @@ -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), + } +) diff --git a/internal/config/make_rule_targets_test.go b/internal/mappingrules/init_rule_targets_test.go similarity index 71% rename from internal/config/make_rule_targets_test.go rename to internal/mappingrules/init_rule_targets_test.go index 7ee8fb8..168b02d 100644 --- a/internal/config/make_rule_targets_test.go +++ b/internal/mappingrules/init_rule_targets_test.go @@ -1,9 +1,12 @@ -package config +// TODO: these tests should live with their rule_target_* counterparts + +package mappingrules import ( "fmt" "testing" + "git.annabunches.net/annabunches/joyful/internal/configparser" "github.com/holoplot/go-evdev" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" @@ -48,45 +51,45 @@ func (t *MakeRuleTargetsTests) SetupSuite() { } func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() { - config := RuleTargetConfigButton{Device: "test"} + config := configparser.RuleTargetConfigButton{Device: "test"} t.Run("Standard keycode", func() { config.Button = "BTN_TRIGGER" - rule, err := makeRuleTargetButton(config, t.devs) + rule, err := NewRuleTargetButtonFromConfig(config, t.devs) t.Nil(err) t.EqualValues(evdev.BTN_TRIGGER, rule.Button) }) t.Run("Hex code", func() { config.Button = "0x2fd" - rule, err := makeRuleTargetButton(config, t.devs) + rule, err := NewRuleTargetButtonFromConfig(config, t.devs) t.Nil(err) t.EqualValues(evdev.EvCode(0x2fd), rule.Button) }) t.Run("Index", func() { config.Button = "3" - rule, err := makeRuleTargetButton(config, t.devs) + rule, err := NewRuleTargetButtonFromConfig(config, t.devs) t.Nil(err) t.EqualValues(evdev.BTN_TOP, rule.Button) }) t.Run("Index too high", func() { config.Button = "74" - _, err := makeRuleTargetButton(config, t.devs) + _, err := NewRuleTargetButtonFromConfig(config, t.devs) t.NotNil(err) }) t.Run("Un-prefixed keycode", func() { config.Button = "pinkie" - rule, err := makeRuleTargetButton(config, t.devs) + rule, err := NewRuleTargetButtonFromConfig(config, t.devs) t.Nil(err) t.EqualValues(evdev.BTN_PINKIE, rule.Button) }) t.Run("Invalid keycode", func() { config.Button = "foo" - _, err := makeRuleTargetButton(config, t.devs) + _, err := NewRuleTargetButtonFromConfig(config, t.devs) t.NotNil(err) }) } @@ -103,9 +106,9 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { for _, tc := range codeTestCases { t.Run(fmt.Sprintf("KeyCode %s", tc.input), func() { - config := RuleTargetConfigAxis{Device: "test"} + config := configparser.RuleTargetConfigAxis{Device: "test"} config.Axis = tc.input - rule, err := makeRuleTargetAxis(config, t.devs) + rule, err := NewRuleTargetAxisFromConfig(config, t.devs) t.Nil(err) t.EqualValues(tc.output, rule.Axis) @@ -113,18 +116,18 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { } t.Run("Invalid code", func() { - config := RuleTargetConfigAxis{Device: "test"} + config := configparser.RuleTargetConfigAxis{Device: "test"} config.Axis = "foo" - _, err := makeRuleTargetAxis(config, t.devs) + _, err := NewRuleTargetAxisFromConfig(config, t.devs) t.NotNil(err) }) t.Run("Invalid deadzone", func() { - config := RuleTargetConfigAxis{Device: "test"} + config := configparser.RuleTargetConfigAxis{Device: "test"} config.Axis = "x" config.DeadzoneEnd = 100 config.DeadzoneStart = 1000 - _, err := makeRuleTargetAxis(config, t.devs) + _, err := NewRuleTargetAxisFromConfig(config, t.devs) t.NotNil(err) }) @@ -141,13 +144,13 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { for _, tc := range relDeadzoneTestCases { t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() { - config := RuleTargetConfigAxis{ + config := configparser.RuleTargetConfigAxis{ Device: "test", Axis: "x", DeadzoneCenter: tc.inCenter, DeadzoneSize: tc.inSize, } - rule, err := makeRuleTargetAxis(config, t.devs) + rule, err := NewRuleTargetAxisFromConfig(config, t.devs) t.Nil(err) t.Equal(tc.outStart, rule.DeadzoneStart) @@ -156,13 +159,13 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { } t.Run("Deadzone center/size invalid center", func() { - config := RuleTargetConfigAxis{ + config := configparser.RuleTargetConfigAxis{ Device: "test", Axis: "x", DeadzoneCenter: 20000, DeadzoneSize: 500, } - _, err := makeRuleTargetAxis(config, t.devs) + _, err := NewRuleTargetAxisFromConfig(config, t.devs) t.NotNil(err) }) @@ -179,13 +182,13 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { for _, tc := range relDeadzonePercentTestCases { t.Run(fmt.Sprintf("Relative percent deadzone %d +- %d%%", tc.inCenter, tc.inSizePercent), func() { - config := RuleTargetConfigAxis{ + config := configparser.RuleTargetConfigAxis{ Device: "test", Axis: "x", DeadzoneCenter: tc.inCenter, DeadzoneSizePercent: tc.inSizePercent, } - rule, err := makeRuleTargetAxis(config, t.devs) + rule, err := NewRuleTargetAxisFromConfig(config, t.devs) t.Nil(err) t.Equal(tc.outStart, rule.DeadzoneStart) @@ -194,50 +197,50 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { } t.Run("Deadzone center/percent invalid center", func() { - config := RuleTargetConfigAxis{ + config := configparser.RuleTargetConfigAxis{ Device: "test", Axis: "x", DeadzoneCenter: 20000, DeadzoneSizePercent: 10, } - _, err := makeRuleTargetAxis(config, t.devs) + _, err := NewRuleTargetAxisFromConfig(config, t.devs) t.NotNil(err) }) } func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() { - config := RuleTargetConfigRelaxis{Device: "test"} + config := configparser.RuleTargetConfigRelaxis{Device: "test"} t.Run("Standard keycode", func() { config.Axis = "REL_WHEEL" - rule, err := makeRuleTargetRelaxis(config, t.devs) + rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs) t.Nil(err) t.EqualValues(evdev.REL_WHEEL, rule.Axis) }) t.Run("Hex keycode", func() { config.Axis = "0x00" - rule, err := makeRuleTargetRelaxis(config, t.devs) + rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs) t.Nil(err) t.EqualValues(evdev.REL_X, rule.Axis) }) t.Run("Un-prefixed keycode", func() { config.Axis = "wheel" - rule, err := makeRuleTargetRelaxis(config, t.devs) + rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs) t.Nil(err) t.EqualValues(evdev.REL_WHEEL, rule.Axis) }) t.Run("Invalid keycode", func() { config.Axis = "foo" - _, err := makeRuleTargetRelaxis(config, t.devs) + _, err := NewRuleTargetRelaxisFromConfig(config, t.devs) t.NotNil(err) }) t.Run("Incorrect axis type", func() { config.Axis = "ABS_X" - _, err := makeRuleTargetRelaxis(config, t.devs) + _, err := NewRuleTargetRelaxisFromConfig(config, t.devs) t.NotNil(err) }) } diff --git a/internal/mappingrules/init_rules.go b/internal/mappingrules/init_rules.go new file mode 100644 index 0000000..7ea0ea4 --- /dev/null +++ b/internal/mappingrules/init_rules.go @@ -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 +} diff --git a/internal/mappingrules/mapping_rule_axis.go b/internal/mappingrules/mapping_rule_axis.go index a2ab41d..a4d1ed1 100644 --- a/internal/mappingrules/mapping_rule_axis.go +++ b/internal/mappingrules/mapping_rule_axis.go @@ -1,6 +1,9 @@ package mappingrules -import "github.com/holoplot/go-evdev" +import ( + "git.annabunches.net/annabunches/joyful/internal/configparser" + "github.com/holoplot/go-evdev" +) // A Simple Mapping Rule can map a button to a button or an axis to an axis. type MappingRuleAxis struct { @@ -9,12 +12,26 @@ type MappingRuleAxis struct { Output *RuleTargetAxis } -func NewMappingRuleAxis(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxis { +func NewMappingRuleAxis(ruleConfig configparser.RuleConfigAxis, + pDevs map[string]Device, + vDevs map[string]Device, + base MappingRuleBase) (*MappingRuleAxis, error) { + + input, err := NewRuleTargetAxisFromConfig(ruleConfig.Input, pDevs) + if err != nil { + return nil, err + } + + output, err := NewRuleTargetAxisFromConfig(ruleConfig.Output, vDevs) + if err != nil { + return nil, err + } + return &MappingRuleAxis{ MappingRuleBase: base, Input: input, Output: output, - } + }, nil } func (rule *MappingRuleAxis) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { diff --git a/internal/mappingrules/mapping_rule_axis_combined.go b/internal/mappingrules/mapping_rule_axis_combined.go index 36562b8..62ce542 100644 --- a/internal/mappingrules/mapping_rule_axis_combined.go +++ b/internal/mappingrules/mapping_rule_axis_combined.go @@ -1,6 +1,7 @@ package mappingrules import ( + "git.annabunches.net/annabunches/joyful/internal/configparser" "git.annabunches.net/annabunches/joyful/internal/logger" "github.com/holoplot/go-evdev" ) @@ -12,7 +13,26 @@ type MappingRuleAxisCombined struct { Output *RuleTargetAxis } -func NewMappingRuleAxisCombined(base MappingRuleBase, inputLower *RuleTargetAxis, inputUpper *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxisCombined { +func NewMappingRuleAxisCombined(ruleConfig configparser.RuleConfigAxisCombined, + pDevs map[string]Device, + vDevs map[string]Device, + base MappingRuleBase) (*MappingRuleAxisCombined, error) { + + inputLower, err := NewRuleTargetAxisFromConfig(ruleConfig.InputLower, pDevs) + if err != nil { + return nil, err + } + + inputUpper, err := NewRuleTargetAxisFromConfig(ruleConfig.InputUpper, pDevs) + if err != nil { + return nil, err + } + + output, err := NewRuleTargetAxisFromConfig(ruleConfig.Output, vDevs) + if err != nil { + return nil, err + } + inputLower.OutputMax = 0 inputUpper.OutputMin = 0 return &MappingRuleAxisCombined{ @@ -20,7 +40,7 @@ func NewMappingRuleAxisCombined(base MappingRuleBase, inputLower *RuleTargetAxis InputLower: inputLower, InputUpper: inputUpper, Output: output, - } + }, nil } func (rule *MappingRuleAxisCombined) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { diff --git a/internal/mappingrules/mapping_rule_axis_combined_test.go b/internal/mappingrules/mapping_rule_axis_combined_test.go index 631d7a0..c514ed7 100644 --- a/internal/mappingrules/mapping_rule_axis_combined_test.go +++ b/internal/mappingrules/mapping_rule_axis_combined_test.go @@ -38,7 +38,9 @@ func (t *MappingRuleAxisCombinedTests) SetupTest() { }, nil) t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, 0, 0) + t.inputTargetLower.OutputMax = 0 t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, 0, 0) + t.inputTargetUpper.OutputMin = 0 t.outputDevice = &evdev.InputDevice{} t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0) @@ -57,19 +59,30 @@ func (t *MappingRuleAxisCombinedTests) TearDownSubTest() { t.inputDevice.Reset() } +// TODO: this test sucks func (t *MappingRuleAxisCombinedTests) TestNewMappingRuleAxisCombined() { t.inputDevice.Stub("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{ evdev.ABS_X: {Minimum: 0, Maximum: 10000}, evdev.ABS_Y: {Minimum: 0, Maximum: 10000}, }, nil) - rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget) + rule := &MappingRuleAxisCombined{ + MappingRuleBase: t.base, + InputLower: t.inputTargetLower, + InputUpper: t.inputTargetUpper, + Output: t.outputTarget, + } t.EqualValues(0, rule.InputLower.OutputMax) t.EqualValues(0, rule.InputUpper.OutputMin) } func (t *MappingRuleAxisCombinedTests) TestMatchEvent() { - rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget) + rule := &MappingRuleAxisCombined{ + MappingRuleBase: t.base, + InputLower: t.inputTargetLower, + InputUpper: t.inputTargetUpper, + Output: t.outputTarget, + } t.Run("Lower Input", func() { testCases := []struct{ in, out int32 }{ diff --git a/internal/mappingrules/mapping_rule_axis_to_button.go b/internal/mappingrules/mapping_rule_axis_to_button.go index 3356dbe..82862ee 100644 --- a/internal/mappingrules/mapping_rule_axis_to_button.go +++ b/internal/mappingrules/mapping_rule_axis_to_button.go @@ -3,6 +3,7 @@ package mappingrules import ( "time" + "git.annabunches.net/annabunches/joyful/internal/configparser" "github.com/holoplot/go-evdev" "github.com/jonboulle/clockwork" ) @@ -23,20 +24,34 @@ type MappingRuleAxisToButton struct { clock clockwork.Clock } -func NewMappingRuleAxisToButton(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetButton, repeatRateMin, repeatRateMax int) *MappingRuleAxisToButton { +func NewMappingRuleAxisToButton(ruleConfig configparser.RuleConfigAxisToButton, + pDevs map[string]Device, + vDevs map[string]Device, + base MappingRuleBase) (*MappingRuleAxisToButton, error) { + + input, err := NewRuleTargetAxisFromConfig(ruleConfig.Input, pDevs) + if err != nil { + return nil, err + } + + output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs) + if err != nil { + return nil, err + } + return &MappingRuleAxisToButton{ MappingRuleBase: base, Input: input, Output: output, - RepeatRateMin: repeatRateMin, - RepeatRateMax: repeatRateMax, + RepeatRateMin: ruleConfig.RepeatRateMin, + RepeatRateMax: ruleConfig.RepeatRateMax, lastEvent: time.Now(), nextEvent: NoNextEvent, - repeat: repeatRateMin != 0 && repeatRateMax != 0, + repeat: ruleConfig.RepeatRateMin != 0 && ruleConfig.RepeatRateMax != 0, pressed: false, active: false, clock: clockwork.NewRealClock(), - } + }, nil } func (rule *MappingRuleAxisToButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { diff --git a/internal/mappingrules/mapping_rule_axis_to_button_test.go b/internal/mappingrules/mapping_rule_axis_to_button_test.go index 976506c..0da086a 100644 --- a/internal/mappingrules/mapping_rule_axis_to_button_test.go +++ b/internal/mappingrules/mapping_rule_axis_to_button_test.go @@ -19,6 +19,44 @@ type MappingRuleAxisToButtonTests struct { base MappingRuleBase } +func TestRunnerMappingRuleAxisToButtonTests(t *testing.T) { + suite.Run(t, new(MappingRuleAxisToButtonTests)) +} + +// buildTimerRule creates a MappingRuleAxisToButton with a mocked clock +func (t *MappingRuleAxisToButtonTests) buildTimerRule( + repeatMin, + repeatMax int, + nextEvent time.Duration) (*MappingRuleAxisToButton, *clockwork.FakeClock) { + + mockClock := clockwork.NewFakeClock() + testRule := t.buildRule(repeatMin, repeatMax) + testRule.clock = mockClock + testRule.lastEvent = testRule.clock.Now() + testRule.nextEvent = nextEvent + if nextEvent != NoNextEvent { + testRule.active = true + } + return testRule, mockClock +} + +// Todo: don't love this repeated logic... +func (t *MappingRuleAxisToButtonTests) buildRule(repeatMin, repeatMax int) *MappingRuleAxisToButton { + return &MappingRuleAxisToButton{ + MappingRuleBase: t.base, + Input: t.inputRule, + Output: t.outputRule, + RepeatRateMin: repeatMin, + RepeatRateMax: repeatMax, + lastEvent: time.Now(), + nextEvent: NoNextEvent, + repeat: repeatMin != 0 && repeatMax != 0, + pressed: false, + active: false, + clock: clockwork.NewRealClock(), + } +} + func (t *MappingRuleAxisToButtonTests) SetupTest() { mode := "*" t.mode = &mode @@ -40,7 +78,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() { // A valid input should set a nextevent t.Run("No Repeat", func() { - testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 0, 0) + testRule := t.buildRule(0, 0) t.Run("Valid Input", func() { testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{ @@ -62,7 +100,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() { }) t.Run("Repeat", func() { - testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 750, 250) + testRule := t.buildRule(750, 250) testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{ Type: evdev.EV_ABS, Code: evdev.ABS_X, @@ -90,7 +128,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() { t.Run("No Repeat", func() { // Get event if called immediately t.Run("Event is available immediately", func() { - testRule, _ := buildTimerRule(t, 0, 0, 0) + testRule, _ := t.buildTimerRule(0, 0, 0) event := testRule.TimerEvent() @@ -100,7 +138,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() { // Off event on second call t.Run("Event emits off on second call", func() { - testRule, _ := buildTimerRule(t, 0, 0, 0) + testRule, _ := t.buildTimerRule(0, 0, 0) testRule.TimerEvent() event := testRule.TimerEvent() @@ -111,7 +149,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() { // No further event, even if we wait a while t.Run("Additional events are not emitted while still active.", func() { - testRule, mockClock := buildTimerRule(t, 0, 0, 0) + testRule, mockClock := t.buildTimerRule(0, 0, 0) testRule.TimerEvent() testRule.TimerEvent() @@ -125,13 +163,13 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() { t.Run("Repeat", func() { t.Run("No event if called immediately", func() { - testRule, _ := buildTimerRule(t, 100, 10, 50*time.Millisecond) + testRule, _ := t.buildTimerRule(100, 10, 50*time.Millisecond) event := testRule.TimerEvent() t.Nil(event) }) t.Run("No event after 49ms", func() { - testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond) + testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond) mockClock.Advance(49 * time.Millisecond) event := testRule.TimerEvent() @@ -140,7 +178,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() { }) t.Run("Event after 50ms", func() { - testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond) + testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond) mockClock.Advance(50 * time.Millisecond) event := testRule.TimerEvent() @@ -150,7 +188,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() { }) t.Run("Additional event at 100ms", func() { - testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond) + testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond) mockClock.Advance(50 * time.Millisecond) testRule.TimerEvent() @@ -163,24 +201,3 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() { }) }) } - -func TestRunnerMappingRuleAxisToButtonTests(t *testing.T) { - suite.Run(t, new(MappingRuleAxisToButtonTests)) -} - -// buildTimerRule creates a MappingRuleAxisToButton with a mocked clock -func buildTimerRule(t *MappingRuleAxisToButtonTests, - repeatMin, - repeatMax int, - nextEvent time.Duration) (*MappingRuleAxisToButton, *clockwork.FakeClock) { - - mockClock := clockwork.NewFakeClock() - testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, repeatMin, repeatMax) - testRule.clock = mockClock - testRule.lastEvent = testRule.clock.Now() - testRule.nextEvent = nextEvent - if nextEvent != NoNextEvent { - testRule.active = true - } - return testRule, mockClock -} diff --git a/internal/mappingrules/mapping_rule_axis_to_relaxis.go b/internal/mappingrules/mapping_rule_axis_to_relaxis.go index 153b992..a6b418e 100644 --- a/internal/mappingrules/mapping_rule_axis_to_relaxis.go +++ b/internal/mappingrules/mapping_rule_axis_to_relaxis.go @@ -3,6 +3,7 @@ package mappingrules import ( "time" + "git.annabunches.net/annabunches/joyful/internal/configparser" "github.com/holoplot/go-evdev" "github.com/jonboulle/clockwork" ) @@ -23,23 +24,32 @@ type MappingRuleAxisToRelaxis struct { clock clockwork.Clock } -func NewMappingRuleAxisToRelaxis( - base MappingRuleBase, - input *RuleTargetAxis, - output *RuleTargetRelaxis, - repeatRateMin, repeatRateMax, increment int) *MappingRuleAxisToRelaxis { +func NewMappingRuleAxisToRelaxis(ruleConfig configparser.RuleConfigAxisToRelaxis, + pDevs map[string]Device, + vDevs map[string]Device, + base MappingRuleBase) (*MappingRuleAxisToRelaxis, error) { + + input, err := NewRuleTargetAxisFromConfig(ruleConfig.Input, pDevs) + if err != nil { + return nil, err + } + + output, err := NewRuleTargetRelaxisFromConfig(ruleConfig.Output, vDevs) + if err != nil { + return nil, err + } return &MappingRuleAxisToRelaxis{ MappingRuleBase: base, Input: input, Output: output, - RepeatRateMin: repeatRateMin, - RepeatRateMax: repeatRateMax, - Increment: int32(increment), + RepeatRateMin: ruleConfig.RepeatRateMin, + RepeatRateMax: ruleConfig.RepeatRateMax, + Increment: int32(ruleConfig.Increment), lastEvent: time.Now(), nextEvent: NoNextEvent, clock: clockwork.NewRealClock(), - } + }, nil } func (rule *MappingRuleAxisToRelaxis) MatchEvent( diff --git a/internal/mappingrules/mapping_rule_button.go b/internal/mappingrules/mapping_rule_button.go index 69a7cfe..3b7befa 100644 --- a/internal/mappingrules/mapping_rule_button.go +++ b/internal/mappingrules/mapping_rule_button.go @@ -1,6 +1,9 @@ package mappingrules -import "github.com/holoplot/go-evdev" +import ( + "git.annabunches.net/annabunches/joyful/internal/configparser" + "github.com/holoplot/go-evdev" +) // A Simple Mapping Rule can map a button to a button or an axis to an axis. type MappingRuleButton struct { @@ -9,16 +12,26 @@ type MappingRuleButton struct { Output *RuleTargetButton } -func NewMappingRuleButton( - base MappingRuleBase, - input *RuleTargetButton, - output *RuleTargetButton) *MappingRuleButton { +func NewMappingRuleButton(ruleConfig configparser.RuleConfigButton, + pDevs map[string]Device, + vDevs map[string]Device, + base MappingRuleBase) (*MappingRuleButton, error) { + + input, err := NewRuleTargetButtonFromConfig(ruleConfig.Input, pDevs) + if err != nil { + return nil, err + } + + output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs) + if err != nil { + return nil, err + } return &MappingRuleButton{ MappingRuleBase: base, Input: input, Output: output, - } + }, nil } func (rule *MappingRuleButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { diff --git a/internal/mappingrules/mapping_rule_button_combo.go b/internal/mappingrules/mapping_rule_button_combo.go index a7b7c23..12c8ef3 100644 --- a/internal/mappingrules/mapping_rule_button_combo.go +++ b/internal/mappingrules/mapping_rule_button_combo.go @@ -1,6 +1,9 @@ package mappingrules -import "github.com/holoplot/go-evdev" +import ( + "git.annabunches.net/annabunches/joyful/internal/configparser" + "github.com/holoplot/go-evdev" +) // A Combo Mapping Rule can require multiple physical button presses for a single output button type MappingRuleButtonCombo struct { @@ -10,17 +13,31 @@ type MappingRuleButtonCombo struct { State int } -func NewMappingRuleButtonCombo( - base MappingRuleBase, - inputs []*RuleTargetButton, - output *RuleTargetButton) *MappingRuleButtonCombo { +func NewMappingRuleButtonCombo(ruleConfig configparser.RuleConfigButtonCombo, + pDevs map[string]Device, + vDevs map[string]Device, + base MappingRuleBase) (*MappingRuleButtonCombo, error) { + + inputs := make([]*RuleTargetButton, 0) + for _, inputConfig := range ruleConfig.Inputs { + input, err := NewRuleTargetButtonFromConfig(inputConfig, pDevs) + if err != nil { + return nil, err + } + inputs = append(inputs, input) + } + + output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs) + if err != nil { + return nil, err + } return &MappingRuleButtonCombo{ MappingRuleBase: base, Inputs: inputs, Output: output, State: 0, - } + }, nil } func (rule *MappingRuleButtonCombo) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { diff --git a/internal/mappingrules/mapping_rule_button_latched.go b/internal/mappingrules/mapping_rule_button_latched.go index d8e5bec..4536ca9 100644 --- a/internal/mappingrules/mapping_rule_button_latched.go +++ b/internal/mappingrules/mapping_rule_button_latched.go @@ -1,6 +1,9 @@ package mappingrules -import "github.com/holoplot/go-evdev" +import ( + "git.annabunches.net/annabunches/joyful/internal/configparser" + "github.com/holoplot/go-evdev" +) type MappingRuleButtonLatched struct { MappingRuleBase @@ -9,17 +12,27 @@ type MappingRuleButtonLatched struct { State bool } -func NewMappingRuleButtonLatched( - base MappingRuleBase, - input *RuleTargetButton, - output *RuleTargetButton) *MappingRuleButtonLatched { +func NewMappingRuleButtonLatched(ruleConfig configparser.RuleConfigButtonLatched, + pDevs map[string]Device, + vDevs map[string]Device, + base MappingRuleBase) (*MappingRuleButtonLatched, error) { + + input, err := NewRuleTargetButtonFromConfig(ruleConfig.Input, pDevs) + if err != nil { + return nil, err + } + + output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs) + if err != nil { + return nil, err + } return &MappingRuleButtonLatched{ MappingRuleBase: base, Input: input, Output: output, State: false, - } + }, nil } func (rule *MappingRuleButtonLatched) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { diff --git a/internal/mappingrules/mapping_rule_button_test.go b/internal/mappingrules/mapping_rule_button_test.go index 28fba1b..740c1ce 100644 --- a/internal/mappingrules/mapping_rule_button_test.go +++ b/internal/mappingrules/mapping_rule_button_test.go @@ -28,7 +28,11 @@ func (t *MappingRuleButtonTests) SetupTest() { func (t *MappingRuleButtonTests) TestMatchEvent() { inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, false) outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false) - testRule := NewMappingRuleButton(t.base, inputButton, outputButton) + testRule := &MappingRuleButton{ + MappingRuleBase: t.base, + Input: inputButton, + Output: outputButton, + } // A matching input event should produce an output event expected := &evdev.InputEvent{ @@ -58,7 +62,11 @@ func (t *MappingRuleButtonTests) TestMatchEvent() { func (t *MappingRuleButtonTests) TestMatchEventInverted() { inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, true) outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false) - testRule := NewMappingRuleButton(t.base, inputButton, outputButton) + testRule := &MappingRuleButton{ + MappingRuleBase: t.base, + Input: inputButton, + Output: outputButton, + } // A matching input event should produce an output event expected := &evdev.InputEvent{ diff --git a/internal/mappingrules/mapping_rule_mode_select.go b/internal/mappingrules/mapping_rule_mode_select.go index 69afd0b..23a0757 100644 --- a/internal/mappingrules/mapping_rule_mode_select.go +++ b/internal/mappingrules/mapping_rule_mode_select.go @@ -1,6 +1,9 @@ package mappingrules -import "github.com/holoplot/go-evdev" +import ( + "git.annabunches.net/annabunches/joyful/internal/configparser" + "github.com/holoplot/go-evdev" +) type MappingRuleModeSelect struct { MappingRuleBase @@ -8,17 +11,26 @@ type MappingRuleModeSelect struct { Output *RuleTargetModeSelect } -func NewMappingRuleModeSelect( - base MappingRuleBase, - input *RuleTargetButton, - output *RuleTargetModeSelect, -) *MappingRuleModeSelect { +func NewMappingRuleModeSelect(ruleConfig configparser.RuleConfigModeSelect, + pDevs map[string]Device, + modes []string, + base MappingRuleBase) (*MappingRuleModeSelect, error) { + + input, err := NewRuleTargetButtonFromConfig(ruleConfig.Input, pDevs) + if err != nil { + return nil, err + } + + output, err := NewRuleTargetModeSelectFromConfig(ruleConfig.Output, modes) + if err != nil { + return nil, err + } return &MappingRuleModeSelect{ MappingRuleBase: base, Input: input, Output: output, - } + }, nil } func (rule *MappingRuleModeSelect) MatchEvent( diff --git a/internal/mappingrules/math.go b/internal/mappingrules/math.go index 37de4a2..6d036df 100644 --- a/internal/mappingrules/math.go +++ b/internal/mappingrules/math.go @@ -28,3 +28,16 @@ func Clamp[T Numeric](value, min, max T) T { } return value } + +func clampAndShift(start, end, min, max int32) (int32, int32) { + if start < min { + end += min - start + start = min + } + if end > max { + start -= end - max + end = max + } + + return start, end +} diff --git a/internal/mappingrules/rule_target_axis.go b/internal/mappingrules/rule_target_axis.go index fece9b8..1d92d37 100644 --- a/internal/mappingrules/rule_target_axis.go +++ b/internal/mappingrules/rule_target_axis.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" + "git.annabunches.net/annabunches/joyful/internal/configparser" + "git.annabunches.net/annabunches/joyful/internal/eventcodes" "github.com/holoplot/go-evdev" ) @@ -20,6 +22,77 @@ type RuleTargetAxis struct { deadzoneSize int32 } +func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis, devs map[string]Device) (*RuleTargetAxis, error) { + device, ok := devs[targetConfig.Device] + if !ok { + return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) + } + + if targetConfig.DeadzoneEnd < targetConfig.DeadzoneStart { + return nil, errors.New("deadzone_end must be greater than deadzone_start") + } + + eventCode, err := eventcodes.ParseCode(targetConfig.Axis, eventcodes.CodePrefixAxis) + if err != nil { + return nil, err + } + + deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode) + if err != nil { + return nil, err + } + + return NewRuleTargetAxis( + targetConfig.Device, + device, + eventCode, + targetConfig.Inverted, + deadzoneStart, + deadzoneEnd, + ) +} + +// calculateDeadzones produces the deadzone start and end values in absolute terms +func calculateDeadzones(targetConfig configparser.RuleTargetConfigAxis, device Device, axis evdev.EvCode) (int32, int32, error) { + + var deadzoneStart, deadzoneEnd int32 + deadzoneStart = 0 + deadzoneEnd = 0 + + if targetConfig.DeadzoneStart != 0 || targetConfig.DeadzoneEnd != 0 { + return targetConfig.DeadzoneStart, targetConfig.DeadzoneEnd, nil + } + + var min, max int32 + absInfoMap, err := device.AbsInfos() + + if err != nil { + min = AxisValueMin + max = AxisValueMax + } else { + absInfo := absInfoMap[axis] + min = absInfo.Minimum + max = absInfo.Maximum + } + + if targetConfig.DeadzoneCenter < min || targetConfig.DeadzoneCenter > max { + return 0, 0, fmt.Errorf("deadzone_center '%d' is out of bounds", targetConfig.DeadzoneCenter) + } + + switch { + case targetConfig.DeadzoneSize != 0: + deadzoneStart = targetConfig.DeadzoneCenter - targetConfig.DeadzoneSize/2 + deadzoneEnd = targetConfig.DeadzoneCenter + targetConfig.DeadzoneSize/2 + case targetConfig.DeadzoneSizePercent != 0: + deadzoneSize := (max - min) / targetConfig.DeadzoneSizePercent + deadzoneStart = targetConfig.DeadzoneCenter - deadzoneSize/2 + deadzoneEnd = targetConfig.DeadzoneCenter + deadzoneSize/2 + } + + deadzoneStart, deadzoneEnd = clampAndShift(deadzoneStart, deadzoneEnd, min, max) + return deadzoneStart, deadzoneEnd, nil +} + func NewRuleTargetAxis(device_name string, device Device, axis evdev.EvCode, diff --git a/internal/mappingrules/rule_target_button.go b/internal/mappingrules/rule_target_button.go index 68fd252..316e7c5 100644 --- a/internal/mappingrules/rule_target_button.go +++ b/internal/mappingrules/rule_target_button.go @@ -1,6 +1,12 @@ package mappingrules -import "github.com/holoplot/go-evdev" +import ( + "fmt" + + "git.annabunches.net/annabunches/joyful/internal/configparser" + "git.annabunches.net/annabunches/joyful/internal/eventcodes" + "github.com/holoplot/go-evdev" +) type RuleTargetButton struct { DeviceName string @@ -9,6 +15,25 @@ type RuleTargetButton struct { Inverted bool } +func NewRuleTargetButtonFromConfig(targetConfig configparser.RuleTargetConfigButton, devs map[string]Device) (*RuleTargetButton, error) { + device, ok := devs[targetConfig.Device] + if !ok { + return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) + } + + eventCode, err := eventcodes.ParseCodeButton(targetConfig.Button) + if err != nil { + return nil, err + } + + return NewRuleTargetButton( + targetConfig.Device, + device, + eventCode, + targetConfig.Inverted, + ) +} + func NewRuleTargetButton(device_name string, device Device, code evdev.EvCode, inverted bool) (*RuleTargetButton, error) { return &RuleTargetButton{ DeviceName: device_name, diff --git a/internal/mappingrules/rule_target_modeselect.go b/internal/mappingrules/rule_target_modeselect.go index 55c8f46..0235700 100644 --- a/internal/mappingrules/rule_target_modeselect.go +++ b/internal/mappingrules/rule_target_modeselect.go @@ -4,6 +4,7 @@ import ( "errors" "slices" + "git.annabunches.net/annabunches/joyful/internal/configparser" "git.annabunches.net/annabunches/joyful/internal/logger" "github.com/holoplot/go-evdev" ) @@ -12,6 +13,14 @@ type RuleTargetModeSelect struct { Modes []string } +func NewRuleTargetModeSelectFromConfig(targetConfig configparser.RuleTargetConfigModeSelect, allModes []string) (*RuleTargetModeSelect, error) { + if ok := validateModes(targetConfig.Modes, allModes); !ok { + return nil, errors.New("undefined mode in mode select list") + } + + return NewRuleTargetModeSelect(targetConfig.Modes) +} + func NewRuleTargetModeSelect(modes []string) (*RuleTargetModeSelect, error) { if len(modes) == 0 { return nil, errors.New("cannot create RuleTargetModeSelect: mode list is empty") diff --git a/internal/mappingrules/rule_target_relaxis.go b/internal/mappingrules/rule_target_relaxis.go index 1942c4b..6b79812 100644 --- a/internal/mappingrules/rule_target_relaxis.go +++ b/internal/mappingrules/rule_target_relaxis.go @@ -1,6 +1,10 @@ package mappingrules import ( + "fmt" + + "git.annabunches.net/annabunches/joyful/internal/configparser" + "git.annabunches.net/annabunches/joyful/internal/eventcodes" "github.com/holoplot/go-evdev" ) @@ -10,12 +14,30 @@ type RuleTargetRelaxis struct { 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, axis evdev.EvCode) (*RuleTargetRelaxis, error) { return &RuleTargetRelaxis{ - DeviceName: device_name, + DeviceName: deviceName, Device: device, Axis: axis, }, nil diff --git a/internal/mappingrules/variables.go b/internal/mappingrules/variables.go new file mode 100644 index 0000000..d9a171b --- /dev/null +++ b/internal/mappingrules/variables.go @@ -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" +) diff --git a/internal/virtualdevice/cleanup.go b/internal/virtualdevice/cleanup.go deleted file mode 100644 index 9839f6b..0000000 --- a/internal/virtualdevice/cleanup.go +++ /dev/null @@ -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) - } - } - } -} diff --git a/internal/virtualdevice/eventbuffer.go b/internal/virtualdevice/eventbuffer.go index 9a46341..5364a5d 100644 --- a/internal/virtualdevice/eventbuffer.go +++ b/internal/virtualdevice/eventbuffer.go @@ -11,13 +11,7 @@ import ( type EventBuffer struct { events []*evdev.InputEvent Device VirtualDevice -} - -func NewEventBuffer(device VirtualDevice) *EventBuffer { - return &EventBuffer{ - events: make([]*evdev.InputEvent, 0, 100), - Device: device, - } + Name string } func (buffer *EventBuffer) AddEvent(event *evdev.InputEvent) { diff --git a/internal/virtualdevice/eventbuffer_test.go b/internal/virtualdevice/eventbuffer_test.go index 515de5f..df8c7ff 100644 --- a/internal/virtualdevice/eventbuffer_test.go +++ b/internal/virtualdevice/eventbuffer_test.go @@ -11,10 +11,11 @@ import ( type EventBufferTests struct { suite.Suite - device *VirtualDeviceMock - writeOneCall *mock.Call + device *VirtualDeviceMock + buffer *EventBuffer } +// Mocks type VirtualDeviceMock struct { mock.Mock } @@ -24,65 +25,65 @@ func (m *VirtualDeviceMock) WriteOne(event *evdev.InputEvent) error { return args.Error(0) } +// Setup func TestRunnerEventBufferTests(t *testing.T) { suite.Run(t, new(EventBufferTests)) } -func (t *EventBufferTests) SetupTest() { - t.device = new(VirtualDeviceMock) -} - func (t *EventBufferTests) SetupSubTest() { t.device = new(VirtualDeviceMock) - t.writeOneCall = t.device.On("WriteOne").Return(nil) -} - -func (t *EventBufferTests) TearDownSubTest() { - t.writeOneCall.Unset() + t.buffer = &EventBuffer{Device: t.device} } +// Tests func (t *EventBufferTests) TestNewEventBuffer() { - buffer := NewEventBuffer(t.device) - t.Equal(t.device, buffer.Device) - t.Len(buffer.events, 0) + t.Equal(t.device, t.buffer.Device) + t.Len(t.buffer.events, 0) } -func (t *EventBufferTests) TestEventBufferAddEvent() { - buffer := NewEventBuffer(t.device) - buffer.AddEvent(&evdev.InputEvent{}) - buffer.AddEvent(&evdev.InputEvent{}) - buffer.AddEvent(&evdev.InputEvent{}) - t.Len(buffer.events, 3) -} - -func (t *EventBufferTests) TestEventBufferSendEvents() { - t.Run("3 Events", func() { - buffer := NewEventBuffer(t.device) - buffer.AddEvent(&evdev.InputEvent{}) - buffer.AddEvent(&evdev.InputEvent{}) - buffer.AddEvent(&evdev.InputEvent{}) - errs := buffer.SendEvents() - - t.Len(errs, 0) - t.device.AssertNumberOfCalls(t.T(), "WriteOne", 4) - }) - - t.Run("No Events", func() { - buffer := NewEventBuffer(t.device) - errs := buffer.SendEvents() - - t.Len(errs, 0) - t.device.AssertNumberOfCalls(t.T(), "WriteOne", 0) - }) - - t.Run("Bad Event", func() { - t.writeOneCall.Unset() - t.writeOneCall = t.device.On("WriteOne").Return(errors.New("Fail")) - - buffer := NewEventBuffer(t.device) - buffer.AddEvent(&evdev.InputEvent{}) - errs := buffer.SendEvents() - t.Len(errs, 2) - }) - +func (t *EventBufferTests) TestEventBuffer() { + + t.Run("AddEvent", func() { + t.buffer.AddEvent(&evdev.InputEvent{}) + t.buffer.AddEvent(&evdev.InputEvent{}) + t.buffer.AddEvent(&evdev.InputEvent{}) + t.Len(t.buffer.events, 3) + }) + + t.Run("SendEvents", func() { + t.Run("3 Events", func() { + writeOneCall := t.device.On("WriteOne").Return(nil) + + t.buffer.AddEvent(&evdev.InputEvent{}) + t.buffer.AddEvent(&evdev.InputEvent{}) + t.buffer.AddEvent(&evdev.InputEvent{}) + errs := t.buffer.SendEvents() + + t.Len(errs, 0) + t.device.AssertNumberOfCalls(t.T(), "WriteOne", 4) + + writeOneCall.Unset() + }) + + t.Run("No Events", func() { + writeOneCall := t.device.On("WriteOne").Return(nil) + + errs := t.buffer.SendEvents() + + t.Len(errs, 0) + t.device.AssertNumberOfCalls(t.T(), "WriteOne", 0) + + writeOneCall.Unset() + }) + + t.Run("Bad Event", func() { + writeOneCall := t.device.On("WriteOne").Return(errors.New("Fail")) + + t.buffer.AddEvent(&evdev.InputEvent{}) + errs := t.buffer.SendEvents() + t.Len(errs, 2) + + writeOneCall.Unset() + }) + }) } diff --git a/internal/virtualdevice/init.go b/internal/virtualdevice/init.go new file mode 100644 index 0000000..14f1c04 --- /dev/null +++ b/internal/virtualdevice/init.go @@ -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 +} diff --git a/internal/config/devices_test.go b/internal/virtualdevice/init_test.go similarity index 91% rename from internal/config/devices_test.go rename to internal/virtualdevice/init_test.go index ad3b624..a6e631c 100644 --- a/internal/config/devices_test.go +++ b/internal/virtualdevice/init_test.go @@ -1,4 +1,4 @@ -package config +package virtualdevice import ( "testing" @@ -7,15 +7,15 @@ import ( "github.com/stretchr/testify/suite" ) -type DevicesConfigTests struct { +type InitTests struct { suite.Suite } -func TestRunnerDevicesConfig(t *testing.T) { - suite.Run(t, new(DevicesConfigTests)) +func TestRunnerInit(t *testing.T) { + suite.Run(t, new(InitTests)) } -func (t *DevicesConfigTests) TestMakeButtons() { +func (t *InitTests) TestMakeButtons() { t.Run("Maximum buttons", func() { buttons := makeButtons(VirtualDeviceMaxButtons, []string{}) t.Equal(VirtualDeviceMaxButtons, len(buttons)) @@ -44,7 +44,7 @@ func (t *DevicesConfigTests) TestMakeButtons() { }) } -func (t *DevicesConfigTests) TestMakeAxes() { +func (t *InitTests) TestMakeAxes() { t.Run("8 axes", func() { axes := makeAxes(8, []string{}) t.Equal(8, len(axes)) @@ -81,7 +81,7 @@ func (t *DevicesConfigTests) TestMakeAxes() { }) } -func (t *DevicesConfigTests) TestMakeRelativeAxes() { +func (t *InitTests) TestMakeRelativeAxes() { t.Run("10 axes", func() { axes := makeRelativeAxes(10, []string{}) t.Equal(10, len(axes)) diff --git a/internal/config/variables.go b/internal/virtualdevice/variables.go similarity index 71% rename from internal/config/variables.go rename to internal/virtualdevice/variables.go index 6e62977..11adb46 100644 --- a/internal/config/variables.go +++ b/internal/virtualdevice/variables.go @@ -1,114 +1,16 @@ -package config +package virtualdevice -import ( - "github.com/holoplot/go-evdev" -) +import "github.com/holoplot/go-evdev" const ( - DeviceTypePhysical = "physical" - DeviceTypeVirtual = "virtual" - DevicePresetKeyboard = "keyboard" DevicePresetGamepad = "gamepad" DevicePresetJoystick = "joystick" 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 ) -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 var ( CapabilitiesPresetGamepad = map[evdev.EvType][]evdev.EvCode{ From 8a903e0703aeec7ff903ed6f8b8e8701393d5c49 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Fri, 5 Sep 2025 21:17:55 +0000 Subject: [PATCH 9/9] Make enum values typed strings (#18) This also moves validation into the parsing process and refactors a bunch of code related to the config. Reviewed-on: https://git.annabunches.net/anna/joyful/pulls/18 Co-authored-by: Anna Rose Wiggins Co-committed-by: Anna Rose Wiggins --- cmd/joyful/config.go | 5 +- internal/configparser/deviceconfig.go | 31 ++++ internal/configparser/deviceconfigphysical.go | 35 +++++ internal/configparser/devicetype.go | 40 ++++++ internal/configparser/ruleconfig.go | 60 ++++++++ internal/configparser/ruletype.go | 53 +++++++ internal/configparser/schema.go | 134 +----------------- internal/configparser/variables.go | 15 -- internal/mappingrules/init_rules.go | 20 +-- internal/mappingrules/variables.go | 12 -- 10 files changed, 232 insertions(+), 173 deletions(-) create mode 100644 internal/configparser/deviceconfig.go create mode 100644 internal/configparser/deviceconfigphysical.go create mode 100644 internal/configparser/devicetype.go create mode 100644 internal/configparser/ruleconfig.go create mode 100644 internal/configparser/ruletype.go delete mode 100644 internal/configparser/variables.go delete mode 100644 internal/mappingrules/variables.go diff --git a/cmd/joyful/config.go b/cmd/joyful/config.go index 2b43380..64d6b2d 100644 --- a/cmd/joyful/config.go +++ b/cmd/joyful/config.go @@ -2,7 +2,6 @@ package main import ( "context" - "strings" "sync" "git.annabunches.net/annabunches/joyful/internal/configparser" @@ -16,7 +15,7 @@ func initPhysicalDevices(conf *configparser.Config) map[string]*evdev.InputDevic pDeviceMap := make(map[string]*evdev.InputDevice) for _, devConfig := range conf.Devices { - if strings.ToLower(devConfig.Type) != configparser.DeviceTypePhysical { + if devConfig.Type != configparser.DeviceTypePhysical { continue } @@ -71,7 +70,7 @@ func initVirtualBuffers(config *configparser.Config) (map[string]*evdev.InputDev vBuffersByDevice := make(map[*evdev.InputDevice]*virtualdevice.EventBuffer) for _, devConfig := range config.Devices { - if strings.ToLower(devConfig.Type) != configparser.DeviceTypeVirtual { + if devConfig.Type != configparser.DeviceTypeVirtual { continue } diff --git a/internal/configparser/deviceconfig.go b/internal/configparser/deviceconfig.go new file mode 100644 index 0000000..eafd8ca --- /dev/null +++ b/internal/configparser/deviceconfig.go @@ -0,0 +1,31 @@ +package configparser + +// These top-level structs use custom unmarshaling to unpack each available sub-type +type DeviceConfig struct { + Type DeviceType + Config interface{} +} + +func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error { + metaConfig := &struct { + Type DeviceType + }{} + err := unmarshal(metaConfig) + if err != nil { + return err + } + dc.Type = metaConfig.Type + + err = nil + switch metaConfig.Type { + case DeviceTypePhysical: + config := DeviceConfigPhysical{} + err = unmarshal(&config) + dc.Config = config + case DeviceTypeVirtual: + config := DeviceConfigVirtual{} + err = unmarshal(&config) + dc.Config = config + } + return err +} diff --git a/internal/configparser/deviceconfigphysical.go b/internal/configparser/deviceconfigphysical.go new file mode 100644 index 0000000..ecb5255 --- /dev/null +++ b/internal/configparser/deviceconfigphysical.go @@ -0,0 +1,35 @@ +package configparser + +type DeviceConfigPhysical struct { + Name string + DeviceName string `yaml:"device_name,omitempty"` + DevicePath string `yaml:"device_path,omitempty"` + Lock bool +} + +// TODO: custom yaml unmarshaling is obtuse; do we really need to do all of this work +// just to set a single default value? +func (dc *DeviceConfigPhysical) UnmarshalYAML(unmarshal func(data interface{}) error) error { + var raw struct { + Name string + DeviceName string `yaml:"device_name"` + DevicePath string `yaml:"device_path"` + Lock bool `yaml:"lock,omitempty"` + } + + // Set non-standard defaults + raw.Lock = true + + err := unmarshal(&raw) + if err != nil { + return err + } + + *dc = DeviceConfigPhysical{ + Name: raw.Name, + DeviceName: raw.DeviceName, + DevicePath: raw.DevicePath, + Lock: raw.Lock, + } + return nil +} diff --git a/internal/configparser/devicetype.go b/internal/configparser/devicetype.go new file mode 100644 index 0000000..7640304 --- /dev/null +++ b/internal/configparser/devicetype.go @@ -0,0 +1,40 @@ +package configparser + +import ( + "fmt" + "strings" +) + +type DeviceType string + +const ( + DeviceTypeNone DeviceType = "" + DeviceTypePhysical DeviceType = "physical" + DeviceTypeVirtual DeviceType = "virtual" +) + +var ( + deviceTypeMap = map[string]DeviceType{ + "physical": DeviceTypePhysical, + "virtual": DeviceTypeVirtual, + } +) + +func ParseDeviceType(in string) (DeviceType, error) { + deviceType, ok := deviceTypeMap[strings.ToLower(in)] + if !ok { + return DeviceTypeNone, fmt.Errorf("invalid rule type '%s'", in) + } + return deviceType, nil +} + +func (rt *DeviceType) UnmarshalYAML(unmarshal func(data interface{}) error) error { + var raw string + err := unmarshal(&raw) + if err != nil { + return err + } + + *rt, err = ParseDeviceType(raw) + return err +} diff --git a/internal/configparser/ruleconfig.go b/internal/configparser/ruleconfig.go new file mode 100644 index 0000000..b41e339 --- /dev/null +++ b/internal/configparser/ruleconfig.go @@ -0,0 +1,60 @@ +package configparser + +type RuleConfig struct { + Type RuleType + Name string + Modes []string + Config interface{} +} + +func (dc *RuleConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error { + metaConfig := &struct { + Type RuleType + Name string + Modes []string + }{} + err := unmarshal(metaConfig) + if err != nil { + return err + } + dc.Type = metaConfig.Type + dc.Name = metaConfig.Name + dc.Modes = metaConfig.Modes + + switch dc.Type { + case RuleTypeButton: + config := RuleConfigButton{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeButtonCombo: + config := RuleConfigButtonCombo{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeButtonLatched: + config := RuleConfigButtonLatched{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeAxis: + config := RuleConfigAxis{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeAxisCombined: + config := RuleConfigAxisCombined{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeAxisToButton: + config := RuleConfigAxisToButton{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeAxisToRelaxis: + config := RuleConfigAxisToRelaxis{} + err = unmarshal(&config) + dc.Config = config + case RuleTypeModeSelect: + config := RuleConfigModeSelect{} + err = unmarshal(&config) + dc.Config = config + } + + return err +} diff --git a/internal/configparser/ruletype.go b/internal/configparser/ruletype.go new file mode 100644 index 0000000..7f43001 --- /dev/null +++ b/internal/configparser/ruletype.go @@ -0,0 +1,53 @@ +package configparser + +import ( + "fmt" + "strings" +) + +// TODO: maybe these want to live somewhere other than configparser? +type RuleType string + +const ( + RuleTypeNone RuleType = "" + RuleTypeButton RuleType = "button" + RuleTypeButtonCombo RuleType = "button-combo" + RuleTypeButtonLatched RuleType = "button-latched" + RuleTypeAxis RuleType = "axis" + RuleTypeAxisCombined RuleType = "axis-combined" + RuleTypeAxisToButton RuleType = "axis-to-button" + RuleTypeAxisToRelaxis RuleType = "axis-to-relaxis" + RuleTypeModeSelect RuleType = "mode-select" +) + +var ( + ruleTypeMap = map[string]RuleType{ + "button": RuleTypeButton, + "button-combo": RuleTypeButtonCombo, + "button-latched": RuleTypeButtonLatched, + "axis": RuleTypeAxis, + "axis-combined": RuleTypeAxisCombined, + "axis-to-button": RuleTypeAxisToButton, + "axis-to-relaxis": RuleTypeAxisToRelaxis, + "mode-select": RuleTypeModeSelect, + } +) + +func ParseRuleType(in string) (RuleType, error) { + ruleType, ok := ruleTypeMap[strings.ToLower(in)] + if !ok { + return RuleTypeNone, fmt.Errorf("invalid rule type '%s'", in) + } + return ruleType, nil +} + +func (rt *RuleType) UnmarshalYAML(unmarshal func(data interface{}) error) error { + var raw string + err := unmarshal(&raw) + if err != nil { + return err + } + + *rt, err = ParseRuleType(raw) + return err +} diff --git a/internal/configparser/schema.go b/internal/configparser/schema.go index 8b70521..942f873 100644 --- a/internal/configparser/schema.go +++ b/internal/configparser/schema.go @@ -1,38 +1,13 @@ -// These types comprise the YAML schema for configuring Joyful. -// The config files will be combined and then unmarshalled into this +// These types comprise the YAML schema that doesn't need custom unmarshalling. 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 @@ -116,110 +91,3 @@ type RuleTargetConfigRelaxis struct { 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 -} diff --git a/internal/configparser/variables.go b/internal/configparser/variables.go deleted file mode 100644 index 77e2b9c..0000000 --- a/internal/configparser/variables.go +++ /dev/null @@ -1,15 +0,0 @@ -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" -) diff --git a/internal/mappingrules/init_rules.go b/internal/mappingrules/init_rules.go index 7ea0ea4..f621875 100644 --- a/internal/mappingrules/init_rules.go +++ b/internal/mappingrules/init_rules.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "slices" - "strings" "git.annabunches.net/annabunches/joyful/internal/configparser" "git.annabunches.net/annabunches/joyful/internal/logger" @@ -33,24 +32,25 @@ func NewRule(config configparser.RuleConfig, pDevs map[string]Device, vDevs map[ base := NewMappingRuleBase(config.Name, config.Modes) - switch strings.ToLower(config.Type) { - case RuleTypeButton: + switch config.Type { + case configparser.RuleTypeButton: newRule, err = NewMappingRuleButton(config.Config.(configparser.RuleConfigButton), pDevs, vDevs, base) - case RuleTypeButtonCombo: + case configparser.RuleTypeButtonCombo: newRule, err = NewMappingRuleButtonCombo(config.Config.(configparser.RuleConfigButtonCombo), pDevs, vDevs, base) - case RuleTypeButtonLatched: + case configparser.RuleTypeButtonLatched: newRule, err = NewMappingRuleButtonLatched(config.Config.(configparser.RuleConfigButtonLatched), pDevs, vDevs, base) - case RuleTypeAxis: + case configparser.RuleTypeAxis: newRule, err = NewMappingRuleAxis(config.Config.(configparser.RuleConfigAxis), pDevs, vDevs, base) - case RuleTypeAxisCombined: + case configparser.RuleTypeAxisCombined: newRule, err = NewMappingRuleAxisCombined(config.Config.(configparser.RuleConfigAxisCombined), pDevs, vDevs, base) - case RuleTypeAxisToButton: + case configparser.RuleTypeAxisToButton: newRule, err = NewMappingRuleAxisToButton(config.Config.(configparser.RuleConfigAxisToButton), pDevs, vDevs, base) - case RuleTypeAxisToRelaxis: + case configparser.RuleTypeAxisToRelaxis: newRule, err = NewMappingRuleAxisToRelaxis(config.Config.(configparser.RuleConfigAxisToRelaxis), pDevs, vDevs, base) - case RuleTypeModeSelect: + case configparser.RuleTypeModeSelect: newRule, err = NewMappingRuleModeSelect(config.Config.(configparser.RuleConfigModeSelect), pDevs, modes, base) default: + // Shouldn't actually be possible to get here... err = fmt.Errorf("bad rule type '%s' for rule '%s'", config.Type, config.Name) } diff --git a/internal/mappingrules/variables.go b/internal/mappingrules/variables.go deleted file mode 100644 index d9a171b..0000000 --- a/internal/mappingrules/variables.go +++ /dev/null @@ -1,12 +0,0 @@ -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" -)