diff --git a/.gitignore b/.gitignore index dd955ab..237ede1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ build/ -target/ \ No newline at end of file +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bbb9cf0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,481 @@ +# 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 = "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" +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 = "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" +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", + "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]] +name = "once_cell_polyfill" +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" +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 = "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" +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 = "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" +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 = "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" +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" + +[[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 new file mode 100644 index 0000000..a380d4f --- /dev/null +++ b/Cargo.toml @@ -0,0 +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/cmd/evinfo/main.go b/cmd/evinfo/main.go index 12a0ecb..c2cc8f0 100644 --- a/cmd/evinfo/main.go +++ b/cmd/evinfo/main.go @@ -5,8 +5,7 @@ import ( "slices" // TODO: using config here feels like bad coupling... ButtonFromIndex might need a refactor / move - - "git.annabunches.net/annabunches/joyful/internal/eventcodes" + "git.annabunches.net/annabunches/joyful/internal/config" "git.annabunches.net/annabunches/joyful/internal/logger" "github.com/holoplot/go-evdev" flag "github.com/spf13/pflag" @@ -21,7 +20,7 @@ func isJoystickLike(device *evdev.InputDevice) bool { if slices.Contains(types, evdev.EV_KEY) { buttons := device.CapableEvents(evdev.EV_KEY) - for _, code := range eventcodes.ButtonFromIndex { + for _, code := range config.ButtonFromIndex { if slices.Contains(buttons, code) { return true } diff --git a/cmd/joyful/config.go b/cmd/joyful/config.go deleted file mode 100644 index 64d6b2d..0000000 --- a/cmd/joyful/config.go +++ /dev/null @@ -1,146 +0,0 @@ -package main - -import ( - "context" - "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 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 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 bcdeccc..17482bf 100644 --- a/cmd/joyful/main.go +++ b/cmd/joyful/main.go @@ -1,15 +1,19 @@ package main import ( + "context" "fmt" "os" "strings" + "sync" "github.com/holoplot/go-evdev" flag "github.com/spf13/pflag" - "git.annabunches.net/annabunches/joyful/internal/configparser" + "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 { @@ -17,6 +21,45 @@ 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]*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 @@ -27,39 +70,32 @@ func main() { // parse configs configDir := getConfigDir(configFlag) - config, err := configparser.ParseConfig(configDir) - logger.FatalIfError(err, "Failed to parse configuration") + config := readConfig(configDir) // initialize TTS tts, err := newTTS(ttsOps) logger.LogIfError(err, "Failed to initialize TTS") // Initialize virtual devices with event buffers - vDevicesByName, vBuffersByName, vBuffersByDevice := initVirtualBuffers(config) + vBuffersByName, vBuffersByDevice := initVirtualBuffers(config) // Initialize physical devices pDevices := initPhysicalDevices(config) - // initialize the mode variables - var mode string - modes := config.Modes - if len(modes) == 0 { - mode = "*" - } else { - mode = config.Modes[0] - } - // Load the rules - rules, eventChannel, cancel, wg := loadRules(config, pDevices, vDevicesByName, modes) + 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 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(modes) > 0 { + if len(config.GetModes()) > 1 { logger.Logf("Initial mode set to '%s'", mode) } @@ -97,18 +133,13 @@ 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. Loading new rules.") - rules, eventChannel, cancel, wg = loadRules(config, pDevices, vDevicesByName, modes) + 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.") } @@ -117,3 +148,37 @@ 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.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 +} diff --git a/internal/eventcodes/codes.go b/internal/config/codes.go similarity index 81% rename from internal/eventcodes/codes.go rename to internal/config/codes.go index a7515a8..c879feb 100644 --- a/internal/eventcodes/codes.go +++ b/internal/config/codes.go @@ -1,4 +1,4 @@ -package eventcodes +package config 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,8 +70,3 @@ 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/eventcodes/codes_test.go b/internal/config/codes_test.go similarity index 94% rename from internal/eventcodes/codes_test.go rename to internal/config/codes_test.go index 4d72526..6e80291 100644 --- a/internal/eventcodes/codes_test.go +++ b/internal/config/codes_test.go @@ -1,4 +1,4 @@ -package eventcodes +package config 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/config/configparser.go b/internal/config/configparser.go new file mode 100644 index 0000000..564c00d --- /dev/null +++ b/internal/config/configparser.go @@ -0,0 +1,77 @@ +// 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 new file mode 100644 index 0000000..9802bff --- /dev/null +++ b/internal/config/devices.go @@ -0,0 +1,217 @@ +package config + +import ( + "fmt" + "strings" + + "git.annabunches.net/annabunches/joyful/internal/logger" + "github.com/holoplot/go-evdev" +) + +// CreateVirtualDevices will register any configured devices with type = virtual +// using /dev/uinput, and return a map of those devices. +// +// This function assumes you have already called Parse() on the config directory. +// +// This function should only be called once, unless you want to create duplicate devices for some reason. +func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice { + deviceMap := make(map[string]*evdev.InputDevice) + + for _, deviceConfig := range parser.config.Devices { + if strings.ToLower(deviceConfig.Type) != DeviceTypeVirtual { + continue + } + + 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 +} + +// ConnectPhysicalDevices 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 should only be called once. +func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevice { + deviceMap := make(map[string]*evdev.InputDevice) + + for _, deviceConfig := range parser.config.Devices { + if strings.ToLower(deviceConfig.Type) != DeviceTypePhysical { + continue + } + + 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/virtualdevice/init_test.go b/internal/config/devices_test.go similarity index 91% rename from internal/virtualdevice/init_test.go rename to internal/config/devices_test.go index a6e631c..ad3b624 100644 --- a/internal/virtualdevice/init_test.go +++ b/internal/config/devices_test.go @@ -1,4 +1,4 @@ -package virtualdevice +package config import ( "testing" @@ -7,15 +7,15 @@ import ( "github.com/stretchr/testify/suite" ) -type InitTests struct { +type DevicesConfigTests struct { suite.Suite } -func TestRunnerInit(t *testing.T) { - suite.Run(t, new(InitTests)) +func TestRunnerDevicesConfig(t *testing.T) { + suite.Run(t, new(DevicesConfigTests)) } -func (t *InitTests) TestMakeButtons() { +func (t *DevicesConfigTests) TestMakeButtons() { t.Run("Maximum buttons", func() { buttons := makeButtons(VirtualDeviceMaxButtons, []string{}) t.Equal(VirtualDeviceMaxButtons, len(buttons)) @@ -44,7 +44,7 @@ func (t *InitTests) TestMakeButtons() { }) } -func (t *InitTests) TestMakeAxes() { +func (t *DevicesConfigTests) TestMakeAxes() { t.Run("8 axes", func() { axes := makeAxes(8, []string{}) t.Equal(8, len(axes)) @@ -81,7 +81,7 @@ func (t *InitTests) TestMakeAxes() { }) } -func (t *InitTests) TestMakeRelativeAxes() { +func (t *DevicesConfigTests) TestMakeRelativeAxes() { t.Run("10 axes", func() { axes := makeRelativeAxes(10, []string{}) t.Equal(10, len(axes)) diff --git a/internal/config/interfaces.go b/internal/config/interfaces.go new file mode 100644 index 0000000..0b9fa42 --- /dev/null +++ b/internal/config/interfaces.go @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..7e8c2eb --- /dev/null +++ b/internal/config/make_rule_targets.go @@ -0,0 +1,146 @@ +package config + +import ( + "errors" + "fmt" + + "git.annabunches.net/annabunches/joyful/internal/mappingrules" + "github.com/holoplot/go-evdev" +) + +func makeRuleTargetButton(targetConfig RuleTargetConfig, 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 RuleTargetConfig, 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 RuleTargetConfig, 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, + targetConfig.Inverted, + ) +} + +func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, 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 RuleTargetConfig, 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_rule_targets_test.go b/internal/config/make_rule_targets_test.go new file mode 100644 index 0000000..6e71fa6 --- /dev/null +++ b/internal/config/make_rule_targets_test.go @@ -0,0 +1,244 @@ +package config + +import ( + "testing" + + "github.com/holoplot/go-evdev" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type MakeRuleTargetsTests struct { + suite.Suite + devs map[string]Device + deviceMock *DeviceMock + config RuleTargetConfig +} + +type DeviceMock struct { + mock.Mock +} + +func (m *DeviceMock) AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error) { + args := m.Called() + return args.Get(0).(map[evdev.EvCode]evdev.AbsInfo), args.Error(1) +} + +func TestRunnerMakeRuleTargets(t *testing.T) { + suite.Run(t, new(MakeRuleTargetsTests)) +} + +func (t *MakeRuleTargetsTests) SetupSuite() { + t.deviceMock = new(DeviceMock) + t.deviceMock.On("AbsInfos").Return( + map[evdev.EvCode]evdev.AbsInfo{ + evdev.ABS_X: { + Minimum: 0, + Maximum: 10000, + }, + evdev.ABS_Y: { + Minimum: 0, + Maximum: 10000, + }, + }, nil, + ) + t.devs = map[string]Device{ + "test": t.deviceMock, + } +} + +func (t *MakeRuleTargetsTests) SetupSubTest() { + t.config = RuleTargetConfig{ + Device: "test", + } +} + +func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() { + t.Run("Standard keycode", func() { + t.config.Button = "BTN_TRIGGER" + rule, err := makeRuleTargetButton(t.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) + 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) + 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) + t.NotNil(err) + }) + + t.Run("Un-prefixed keycode", func() { + t.config.Button = "pinkie" + rule, err := makeRuleTargetButton(t.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) + 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) + }) + + 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) + }) + + 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) + 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) + 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) + }) + + 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) + }) + + 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.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) + 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) + }) + + 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) + }) + + 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.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) + t.NotNil(err) + }) +} + +func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() { + t.Run("Standard keycode", func() { + t.config.Axis = "REL_WHEEL" + rule, err := makeRuleTargetRelaxis(t.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) + 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) + 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) + t.NotNil(err) + }) + + t.Run("Incorrect axis type", func() { + t.config.Axis = "ABS_X" + _, err := makeRuleTargetRelaxis(t.config, t.devs) + t.NotNil(err) + }) +} diff --git a/internal/config/make_rules.go b/internal/config/make_rules.go new file mode 100644 index 0000000..647987c --- /dev/null +++ b/internal/config/make_rules.go @@ -0,0 +1,230 @@ +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) BuildRules(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, pDevs, vDevs, base) + case RuleTypeButtonCombo: + newRule, err = makeMappingRuleCombo(ruleConfig, pDevs, vDevs, base) + case RuleTypeLatched: + newRule, err = makeMappingRuleLatched(ruleConfig, pDevs, vDevs, base) + case RuleTypeAxis: + newRule, err = makeMappingRuleAxis(ruleConfig, pDevs, vDevs, base) + case RuleTypeAxisCombined: + newRule, err = makeMappingRuleAxisCombined(ruleConfig, pDevs, vDevs, base) + case RuleTypeAxisToButton: + newRule, err = makeMappingRuleAxisToButton(ruleConfig, pDevs, vDevs, base) + case RuleTypeAxisToRelaxis: + newRule, err = makeMappingRuleAxisToRelaxis(ruleConfig, pDevs, vDevs, base) + case RuleTypeModeSelect: + newRule, err = makeMappingRuleModeSelect(ruleConfig, 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 +} + +func makeMappingRuleButton(ruleConfig RuleConfig, + 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 RuleConfig, + 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 RuleConfig, + 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 RuleConfig, + 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 RuleConfig, + 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 RuleConfig, + 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 RuleConfig, + 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 RuleConfig, + 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 new file mode 100644 index 0000000..ad3dee2 --- /dev/null +++ b/internal/config/modes.go @@ -0,0 +1,19 @@ +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/config/schema.go b/internal/config/schema.go new file mode 100644 index 0000000..1ea3527 --- /dev/null +++ b/internal/config/schema.go @@ -0,0 +1,99 @@ +// 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 + +type Config struct { + Devices []DeviceConfig `yaml:"devices"` + Modes []string `yaml:"modes,omitempty"` + Rules []RuleConfig `yaml:"rules"` +} + +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 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 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"` +} + +// 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 { + 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"` + } + raw.Lock = true + + err := unmarshal(&raw) + if err != nil { + 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, + } + return nil +} diff --git a/internal/virtualdevice/variables.go b/internal/config/variables.go similarity index 71% rename from internal/virtualdevice/variables.go rename to internal/config/variables.go index 11adb46..e4e0bf0 100644 --- a/internal/virtualdevice/variables.go +++ b/internal/config/variables.go @@ -1,16 +1,114 @@ -package virtualdevice +package config -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" + RuleTypeLatched = "button-latched" + RuleTypeAxis = "axis" + RuleTypeAxisCombined = "axis-combined" + RuleTypeModeSelect = "mode-select" + RuleTypeAxisToButton = "axis-to-button" + RuleTypeAxisToRelaxis = "axis-to-relaxis" + + 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{ diff --git a/internal/configparser/configparser.go b/internal/configparser/configparser.go deleted file mode 100644 index 3daa217..0000000 --- a/internal/configparser/configparser.go +++ /dev/null @@ -1,67 +0,0 @@ -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/configparser/deviceconfig.go b/internal/configparser/deviceconfig.go deleted file mode 100644 index eafd8ca..0000000 --- a/internal/configparser/deviceconfig.go +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index ecb5255..0000000 --- a/internal/configparser/deviceconfigphysical.go +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 7640304..0000000 --- a/internal/configparser/devicetype.go +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index b41e339..0000000 --- a/internal/configparser/ruleconfig.go +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 7f43001..0000000 --- a/internal/configparser/ruletype.go +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 942f873..0000000 --- a/internal/configparser/schema.go +++ /dev/null @@ -1,93 +0,0 @@ -// These types comprise the YAML schema that doesn't need custom unmarshalling. - -package configparser - -type Config struct { - Devices []DeviceConfig - Modes []string - Rules []RuleConfig -} - -// 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 -} diff --git a/internal/eventcodes/variables.go b/internal/eventcodes/variables.go deleted file mode 100644 index d63b92d..0000000 --- a/internal/eventcodes/variables.go +++ /dev/null @@ -1,90 +0,0 @@ -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/mappingrules/init_rule_targets_test.go b/internal/mappingrules/init_rule_targets_test.go deleted file mode 100644 index 168b02d..0000000 --- a/internal/mappingrules/init_rule_targets_test.go +++ /dev/null @@ -1,246 +0,0 @@ -// 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" -) - -type MakeRuleTargetsTests struct { - suite.Suite - devs map[string]Device - deviceMock *DeviceMock -} - -type DeviceMock struct { - mock.Mock -} - -func (m *DeviceMock) AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error) { - args := m.Called() - return args.Get(0).(map[evdev.EvCode]evdev.AbsInfo), args.Error(1) -} - -func TestRunnerMakeRuleTargets(t *testing.T) { - suite.Run(t, new(MakeRuleTargetsTests)) -} - -func (t *MakeRuleTargetsTests) SetupSuite() { - t.deviceMock = new(DeviceMock) - t.deviceMock.On("AbsInfos").Return( - map[evdev.EvCode]evdev.AbsInfo{ - evdev.ABS_X: { - Minimum: 0, - Maximum: 10000, - }, - evdev.ABS_Y: { - Minimum: 0, - Maximum: 10000, - }, - }, nil, - ) - t.devs = map[string]Device{ - "test": t.deviceMock, - } -} - -func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() { - config := configparser.RuleTargetConfigButton{Device: "test"} - - t.Run("Standard keycode", func() { - config.Button = "BTN_TRIGGER" - 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 := NewRuleTargetButtonFromConfig(config, t.devs) - t.Nil(err) - t.EqualValues(evdev.EvCode(0x2fd), rule.Button) - }) - - t.Run("Index", func() { - config.Button = "3" - 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 := NewRuleTargetButtonFromConfig(config, t.devs) - t.NotNil(err) - }) - - t.Run("Un-prefixed keycode", func() { - config.Button = "pinkie" - 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 := NewRuleTargetButtonFromConfig(config, t.devs) - t.NotNil(err) - }) -} - -func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { - codeTestCases := []struct { - input string - output evdev.EvCode - }{ - {"ABS_X", evdev.ABS_X}, - {"0x01", evdev.ABS_Y}, - {"x", evdev.ABS_X}, - } - - for _, tc := range codeTestCases { - t.Run(fmt.Sprintf("KeyCode %s", tc.input), func() { - config := configparser.RuleTargetConfigAxis{Device: "test"} - config.Axis = tc.input - rule, err := NewRuleTargetAxisFromConfig(config, t.devs) - t.Nil(err) - t.EqualValues(tc.output, rule.Axis) - - }) - } - - t.Run("Invalid code", func() { - config := configparser.RuleTargetConfigAxis{Device: "test"} - config.Axis = "foo" - _, err := NewRuleTargetAxisFromConfig(config, t.devs) - t.NotNil(err) - }) - - t.Run("Invalid deadzone", func() { - config := configparser.RuleTargetConfigAxis{Device: "test"} - config.Axis = "x" - config.DeadzoneEnd = 100 - config.DeadzoneStart = 1000 - _, err := NewRuleTargetAxisFromConfig(config, t.devs) - t.NotNil(err) - }) - - relDeadzoneTestCases := []struct { - inCenter int32 - inSize int32 - outStart int32 - outEnd int32 - }{ - {5000, 1000, 4500, 5500}, - {0, 500, 0, 500}, - {10000, 500, 9500, 10000}, - } - - for _, tc := range relDeadzoneTestCases { - t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() { - config := configparser.RuleTargetConfigAxis{ - Device: "test", - Axis: "x", - DeadzoneCenter: tc.inCenter, - DeadzoneSize: tc.inSize, - } - rule, err := NewRuleTargetAxisFromConfig(config, t.devs) - - t.Nil(err) - t.Equal(tc.outStart, rule.DeadzoneStart) - t.Equal(tc.outEnd, rule.DeadzoneEnd) - }) - } - - t.Run("Deadzone center/size invalid center", func() { - config := configparser.RuleTargetConfigAxis{ - Device: "test", - Axis: "x", - DeadzoneCenter: 20000, - DeadzoneSize: 500, - } - _, err := NewRuleTargetAxisFromConfig(config, t.devs) - t.NotNil(err) - }) - - relDeadzonePercentTestCases := []struct { - inCenter int32 - inSizePercent int32 - outStart int32 - outEnd int32 - }{ - {5000, 10, 4500, 5500}, - {0, 10, 0, 1000}, - {10000, 10, 9000, 10000}, - } - - for _, tc := range relDeadzonePercentTestCases { - t.Run(fmt.Sprintf("Relative percent deadzone %d +- %d%%", tc.inCenter, tc.inSizePercent), func() { - config := configparser.RuleTargetConfigAxis{ - Device: "test", - Axis: "x", - DeadzoneCenter: tc.inCenter, - DeadzoneSizePercent: tc.inSizePercent, - } - rule, err := NewRuleTargetAxisFromConfig(config, t.devs) - - t.Nil(err) - t.Equal(tc.outStart, rule.DeadzoneStart) - t.Equal(tc.outEnd, rule.DeadzoneEnd) - }) - } - - t.Run("Deadzone center/percent invalid center", func() { - config := configparser.RuleTargetConfigAxis{ - Device: "test", - Axis: "x", - DeadzoneCenter: 20000, - DeadzoneSizePercent: 10, - } - _, err := NewRuleTargetAxisFromConfig(config, t.devs) - t.NotNil(err) - }) -} - -func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() { - config := configparser.RuleTargetConfigRelaxis{Device: "test"} - - t.Run("Standard keycode", func() { - config.Axis = "REL_WHEEL" - 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 := 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 := NewRuleTargetRelaxisFromConfig(config, t.devs) - t.Nil(err) - t.EqualValues(evdev.REL_WHEEL, rule.Axis) - }) - - t.Run("Invalid keycode", func() { - config.Axis = "foo" - _, err := NewRuleTargetRelaxisFromConfig(config, t.devs) - t.NotNil(err) - }) - - t.Run("Incorrect axis type", func() { - config.Axis = "ABS_X" - _, err := NewRuleTargetRelaxisFromConfig(config, t.devs) - t.NotNil(err) - }) -} diff --git a/internal/mappingrules/init_rules.go b/internal/mappingrules/init_rules.go deleted file mode 100644 index f621875..0000000 --- a/internal/mappingrules/init_rules.go +++ /dev/null @@ -1,79 +0,0 @@ -package mappingrules - -import ( - "errors" - "fmt" - "slices" - - "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 config.Type { - case configparser.RuleTypeButton: - newRule, err = NewMappingRuleButton(config.Config.(configparser.RuleConfigButton), pDevs, vDevs, base) - case configparser.RuleTypeButtonCombo: - newRule, err = NewMappingRuleButtonCombo(config.Config.(configparser.RuleConfigButtonCombo), pDevs, vDevs, base) - case configparser.RuleTypeButtonLatched: - newRule, err = NewMappingRuleButtonLatched(config.Config.(configparser.RuleConfigButtonLatched), pDevs, vDevs, base) - case configparser.RuleTypeAxis: - newRule, err = NewMappingRuleAxis(config.Config.(configparser.RuleConfigAxis), pDevs, vDevs, base) - case configparser.RuleTypeAxisCombined: - newRule, err = NewMappingRuleAxisCombined(config.Config.(configparser.RuleConfigAxisCombined), pDevs, vDevs, base) - case configparser.RuleTypeAxisToButton: - newRule, err = NewMappingRuleAxisToButton(config.Config.(configparser.RuleConfigAxisToButton), pDevs, vDevs, base) - case configparser.RuleTypeAxisToRelaxis: - newRule, err = NewMappingRuleAxisToRelaxis(config.Config.(configparser.RuleConfigAxisToRelaxis), pDevs, vDevs, base) - 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) - } - - 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 a4d1ed1..a2ab41d 100644 --- a/internal/mappingrules/mapping_rule_axis.go +++ b/internal/mappingrules/mapping_rule_axis.go @@ -1,9 +1,6 @@ package mappingrules -import ( - "git.annabunches.net/annabunches/joyful/internal/configparser" - "github.com/holoplot/go-evdev" -) +import "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 { @@ -12,26 +9,12 @@ type MappingRuleAxis struct { Output *RuleTargetAxis } -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 - } - +func NewMappingRuleAxis(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxis { 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 62ce542..36562b8 100644 --- a/internal/mappingrules/mapping_rule_axis_combined.go +++ b/internal/mappingrules/mapping_rule_axis_combined.go @@ -1,7 +1,6 @@ package mappingrules import ( - "git.annabunches.net/annabunches/joyful/internal/configparser" "git.annabunches.net/annabunches/joyful/internal/logger" "github.com/holoplot/go-evdev" ) @@ -13,26 +12,7 @@ type MappingRuleAxisCombined struct { Output *RuleTargetAxis } -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 - } - +func NewMappingRuleAxisCombined(base MappingRuleBase, inputLower *RuleTargetAxis, inputUpper *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxisCombined { inputLower.OutputMax = 0 inputUpper.OutputMin = 0 return &MappingRuleAxisCombined{ @@ -40,7 +20,7 @@ func NewMappingRuleAxisCombined(ruleConfig configparser.RuleConfigAxisCombined, 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 c514ed7..631d7a0 100644 --- a/internal/mappingrules/mapping_rule_axis_combined_test.go +++ b/internal/mappingrules/mapping_rule_axis_combined_test.go @@ -38,9 +38,7 @@ 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) @@ -59,30 +57,19 @@ 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 := &MappingRuleAxisCombined{ - MappingRuleBase: t.base, - InputLower: t.inputTargetLower, - InputUpper: t.inputTargetUpper, - Output: t.outputTarget, - } + rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget) t.EqualValues(0, rule.InputLower.OutputMax) t.EqualValues(0, rule.InputUpper.OutputMin) } func (t *MappingRuleAxisCombinedTests) TestMatchEvent() { - rule := &MappingRuleAxisCombined{ - MappingRuleBase: t.base, - InputLower: t.inputTargetLower, - InputUpper: t.inputTargetUpper, - Output: t.outputTarget, - } + rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, 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 82862ee..3356dbe 100644 --- a/internal/mappingrules/mapping_rule_axis_to_button.go +++ b/internal/mappingrules/mapping_rule_axis_to_button.go @@ -3,7 +3,6 @@ package mappingrules import ( "time" - "git.annabunches.net/annabunches/joyful/internal/configparser" "github.com/holoplot/go-evdev" "github.com/jonboulle/clockwork" ) @@ -24,34 +23,20 @@ type MappingRuleAxisToButton struct { clock clockwork.Clock } -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 - } - +func NewMappingRuleAxisToButton(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetButton, repeatRateMin, repeatRateMax int) *MappingRuleAxisToButton { return &MappingRuleAxisToButton{ MappingRuleBase: base, Input: input, Output: output, - RepeatRateMin: ruleConfig.RepeatRateMin, - RepeatRateMax: ruleConfig.RepeatRateMax, + RepeatRateMin: repeatRateMin, + RepeatRateMax: repeatRateMax, lastEvent: time.Now(), nextEvent: NoNextEvent, - repeat: ruleConfig.RepeatRateMin != 0 && ruleConfig.RepeatRateMax != 0, + repeat: repeatRateMin != 0 && 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 0da086a..976506c 100644 --- a/internal/mappingrules/mapping_rule_axis_to_button_test.go +++ b/internal/mappingrules/mapping_rule_axis_to_button_test.go @@ -19,44 +19,6 @@ 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 @@ -78,7 +40,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() { // A valid input should set a nextevent t.Run("No Repeat", func() { - testRule := t.buildRule(0, 0) + testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 0, 0) t.Run("Valid Input", func() { testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{ @@ -100,7 +62,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() { }) t.Run("Repeat", func() { - testRule := t.buildRule(750, 250) + testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 750, 250) testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{ Type: evdev.EV_ABS, Code: evdev.ABS_X, @@ -128,7 +90,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() { t.Run("No Repeat", func() { // Get event if called immediately t.Run("Event is available immediately", func() { - testRule, _ := t.buildTimerRule(0, 0, 0) + testRule, _ := buildTimerRule(t, 0, 0, 0) event := testRule.TimerEvent() @@ -138,7 +100,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() { // Off event on second call t.Run("Event emits off on second call", func() { - testRule, _ := t.buildTimerRule(0, 0, 0) + testRule, _ := buildTimerRule(t, 0, 0, 0) testRule.TimerEvent() event := testRule.TimerEvent() @@ -149,7 +111,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 := t.buildTimerRule(0, 0, 0) + testRule, mockClock := buildTimerRule(t, 0, 0, 0) testRule.TimerEvent() testRule.TimerEvent() @@ -163,13 +125,13 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() { t.Run("Repeat", func() { t.Run("No event if called immediately", func() { - testRule, _ := t.buildTimerRule(100, 10, 50*time.Millisecond) + testRule, _ := buildTimerRule(t, 100, 10, 50*time.Millisecond) event := testRule.TimerEvent() t.Nil(event) }) t.Run("No event after 49ms", func() { - testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond) + testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond) mockClock.Advance(49 * time.Millisecond) event := testRule.TimerEvent() @@ -178,7 +140,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() { }) t.Run("Event after 50ms", func() { - testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond) + testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond) mockClock.Advance(50 * time.Millisecond) event := testRule.TimerEvent() @@ -188,7 +150,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() { }) t.Run("Additional event at 100ms", func() { - testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond) + testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond) mockClock.Advance(50 * time.Millisecond) testRule.TimerEvent() @@ -201,3 +163,24 @@ 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 a6b418e..153b992 100644 --- a/internal/mappingrules/mapping_rule_axis_to_relaxis.go +++ b/internal/mappingrules/mapping_rule_axis_to_relaxis.go @@ -3,7 +3,6 @@ package mappingrules import ( "time" - "git.annabunches.net/annabunches/joyful/internal/configparser" "github.com/holoplot/go-evdev" "github.com/jonboulle/clockwork" ) @@ -24,32 +23,23 @@ type MappingRuleAxisToRelaxis struct { clock clockwork.Clock } -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 - } +func NewMappingRuleAxisToRelaxis( + base MappingRuleBase, + input *RuleTargetAxis, + output *RuleTargetRelaxis, + repeatRateMin, repeatRateMax, increment int) *MappingRuleAxisToRelaxis { return &MappingRuleAxisToRelaxis{ MappingRuleBase: base, Input: input, Output: output, - RepeatRateMin: ruleConfig.RepeatRateMin, - RepeatRateMax: ruleConfig.RepeatRateMax, - Increment: int32(ruleConfig.Increment), + RepeatRateMin: repeatRateMin, + RepeatRateMax: repeatRateMax, + Increment: int32(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 3b7befa..69a7cfe 100644 --- a/internal/mappingrules/mapping_rule_button.go +++ b/internal/mappingrules/mapping_rule_button.go @@ -1,9 +1,6 @@ package mappingrules -import ( - "git.annabunches.net/annabunches/joyful/internal/configparser" - "github.com/holoplot/go-evdev" -) +import "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 { @@ -12,26 +9,16 @@ type MappingRuleButton struct { Output *RuleTargetButton } -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 - } +func NewMappingRuleButton( + base MappingRuleBase, + input *RuleTargetButton, + output *RuleTargetButton) *MappingRuleButton { 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 12c8ef3..a7b7c23 100644 --- a/internal/mappingrules/mapping_rule_button_combo.go +++ b/internal/mappingrules/mapping_rule_button_combo.go @@ -1,9 +1,6 @@ package mappingrules -import ( - "git.annabunches.net/annabunches/joyful/internal/configparser" - "github.com/holoplot/go-evdev" -) +import "github.com/holoplot/go-evdev" // A Combo Mapping Rule can require multiple physical button presses for a single output button type MappingRuleButtonCombo struct { @@ -13,31 +10,17 @@ type MappingRuleButtonCombo struct { State int } -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 - } +func NewMappingRuleButtonCombo( + base MappingRuleBase, + inputs []*RuleTargetButton, + output *RuleTargetButton) *MappingRuleButtonCombo { 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 4536ca9..d8e5bec 100644 --- a/internal/mappingrules/mapping_rule_button_latched.go +++ b/internal/mappingrules/mapping_rule_button_latched.go @@ -1,9 +1,6 @@ package mappingrules -import ( - "git.annabunches.net/annabunches/joyful/internal/configparser" - "github.com/holoplot/go-evdev" -) +import "github.com/holoplot/go-evdev" type MappingRuleButtonLatched struct { MappingRuleBase @@ -12,27 +9,17 @@ type MappingRuleButtonLatched struct { State bool } -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 - } +func NewMappingRuleButtonLatched( + base MappingRuleBase, + input *RuleTargetButton, + output *RuleTargetButton) *MappingRuleButtonLatched { 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 740c1ce..28fba1b 100644 --- a/internal/mappingrules/mapping_rule_button_test.go +++ b/internal/mappingrules/mapping_rule_button_test.go @@ -28,11 +28,7 @@ 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 := &MappingRuleButton{ - MappingRuleBase: t.base, - Input: inputButton, - Output: outputButton, - } + testRule := NewMappingRuleButton(t.base, inputButton, outputButton) // A matching input event should produce an output event expected := &evdev.InputEvent{ @@ -62,11 +58,7 @@ 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 := &MappingRuleButton{ - MappingRuleBase: t.base, - Input: inputButton, - Output: outputButton, - } + testRule := NewMappingRuleButton(t.base, inputButton, 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 23a0757..69afd0b 100644 --- a/internal/mappingrules/mapping_rule_mode_select.go +++ b/internal/mappingrules/mapping_rule_mode_select.go @@ -1,9 +1,6 @@ package mappingrules -import ( - "git.annabunches.net/annabunches/joyful/internal/configparser" - "github.com/holoplot/go-evdev" -) +import "github.com/holoplot/go-evdev" type MappingRuleModeSelect struct { MappingRuleBase @@ -11,26 +8,17 @@ type MappingRuleModeSelect struct { Output *RuleTargetModeSelect } -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 - } +func NewMappingRuleModeSelect( + base MappingRuleBase, + input *RuleTargetButton, + output *RuleTargetModeSelect, +) *MappingRuleModeSelect { 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 6d036df..37de4a2 100644 --- a/internal/mappingrules/math.go +++ b/internal/mappingrules/math.go @@ -28,16 +28,3 @@ 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 1d92d37..fece9b8 100644 --- a/internal/mappingrules/rule_target_axis.go +++ b/internal/mappingrules/rule_target_axis.go @@ -4,8 +4,6 @@ import ( "errors" "fmt" - "git.annabunches.net/annabunches/joyful/internal/configparser" - "git.annabunches.net/annabunches/joyful/internal/eventcodes" "github.com/holoplot/go-evdev" ) @@ -22,77 +20,6 @@ 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 316e7c5..68fd252 100644 --- a/internal/mappingrules/rule_target_button.go +++ b/internal/mappingrules/rule_target_button.go @@ -1,12 +1,6 @@ package mappingrules -import ( - "fmt" - - "git.annabunches.net/annabunches/joyful/internal/configparser" - "git.annabunches.net/annabunches/joyful/internal/eventcodes" - "github.com/holoplot/go-evdev" -) +import "github.com/holoplot/go-evdev" type RuleTargetButton struct { DeviceName string @@ -15,25 +9,6 @@ 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 0235700..55c8f46 100644 --- a/internal/mappingrules/rule_target_modeselect.go +++ b/internal/mappingrules/rule_target_modeselect.go @@ -4,7 +4,6 @@ import ( "errors" "slices" - "git.annabunches.net/annabunches/joyful/internal/configparser" "git.annabunches.net/annabunches/joyful/internal/logger" "github.com/holoplot/go-evdev" ) @@ -13,14 +12,6 @@ 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 6b79812..8de8c0b 100644 --- a/internal/mappingrules/rule_target_relaxis.go +++ b/internal/mappingrules/rule_target_relaxis.go @@ -1,10 +1,6 @@ package mappingrules import ( - "fmt" - - "git.annabunches.net/annabunches/joyful/internal/configparser" - "git.annabunches.net/annabunches/joyful/internal/eventcodes" "github.com/holoplot/go-evdev" ) @@ -12,34 +8,19 @@ type RuleTargetRelaxis struct { DeviceName string Device Device Axis evdev.EvCode + Inverted bool } -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, +func NewRuleTargetRelaxis(device_name string, device Device, - axis evdev.EvCode) (*RuleTargetRelaxis, error) { + axis evdev.EvCode, + inverted bool) (*RuleTargetRelaxis, error) { return &RuleTargetRelaxis{ - DeviceName: deviceName, + DeviceName: device_name, Device: device, Axis: axis, + Inverted: inverted, }, nil } diff --git a/internal/virtualdevice/cleanup.go b/internal/virtualdevice/cleanup.go new file mode 100644 index 0000000..9839f6b --- /dev/null +++ b/internal/virtualdevice/cleanup.go @@ -0,0 +1,35 @@ +// 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 5364a5d..9a46341 100644 --- a/internal/virtualdevice/eventbuffer.go +++ b/internal/virtualdevice/eventbuffer.go @@ -11,7 +11,13 @@ import ( type EventBuffer struct { events []*evdev.InputEvent Device VirtualDevice - Name string +} + +func NewEventBuffer(device VirtualDevice) *EventBuffer { + return &EventBuffer{ + events: make([]*evdev.InputEvent, 0, 100), + Device: device, + } } func (buffer *EventBuffer) AddEvent(event *evdev.InputEvent) { diff --git a/internal/virtualdevice/eventbuffer_test.go b/internal/virtualdevice/eventbuffer_test.go index df8c7ff..515de5f 100644 --- a/internal/virtualdevice/eventbuffer_test.go +++ b/internal/virtualdevice/eventbuffer_test.go @@ -11,11 +11,10 @@ import ( type EventBufferTests struct { suite.Suite - device *VirtualDeviceMock - buffer *EventBuffer + device *VirtualDeviceMock + writeOneCall *mock.Call } -// Mocks type VirtualDeviceMock struct { mock.Mock } @@ -25,65 +24,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.buffer = &EventBuffer{Device: t.device} + t.writeOneCall = t.device.On("WriteOne").Return(nil) +} + +func (t *EventBufferTests) TearDownSubTest() { + t.writeOneCall.Unset() } -// Tests func (t *EventBufferTests) TestNewEventBuffer() { - t.Equal(t.device, t.buffer.Device) - t.Len(t.buffer.events, 0) + buffer := NewEventBuffer(t.device) + t.Equal(t.device, buffer.Device) + t.Len(buffer.events, 0) } -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() - }) - }) +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) + }) + } diff --git a/internal/virtualdevice/init.go b/internal/virtualdevice/init.go deleted file mode 100644 index 14f1c04..0000000 --- a/internal/virtualdevice/init.go +++ /dev/null @@ -1,165 +0,0 @@ -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/src/bin/evinfo.rs b/src/bin/evinfo.rs new file mode 100644 index 0000000..a9d7648 --- /dev/null +++ b/src/bin/evinfo.rs @@ -0,0 +1,84 @@ +use std::path::PathBuf; + +use clap::Parser; +use evdev::raw_stream::RawDevice; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Print additional information about each device. (-vv for even more verbosity) + #[arg(short, long, action = clap::ArgAction::Count)] + verbose: u8, +} + +fn main() { + let args = Args::parse(); + let devices = evdev::raw_stream::enumerate(); + + for (path, dev) in devices { + if is_joystick_like(&dev) { + print_device(path, dev, args.verbose) + } + } +} + +static JOYSTICK_BUTTONS: [evdev::KeyCode; 11] = [ + 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, + evdev::KeyCode::BTN_TRIGGER_HAPPY11, +]; + +fn is_joystick_like(device: &RawDevice) -> bool { + if let Some(_) = device.supported_absolute_axes() { + return true; + } + + if let Some(keys) = device.supported_keys() { + for key in keys.iter() { + if JOYSTICK_BUTTONS.contains(&key) { + return true; + } + } + } + + return false; +} + +fn print_device(path: PathBuf, device: RawDevice, verbose: u8) { + println!( + "{}: \"{}\"", + 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("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 { + if let Ok(abs_info) = device.get_absinfo() { + if abs_info.count() > 0 { + println!("\tAxis Data:"); + for (axis, info) in abs_info { + println!("\t\t{} {}-{}", axis, info.minimum(), info.maximum()); + } + } + } + } + + if verbose > 0 { + println!(); + } +} diff --git a/src/bin/joyful.rs b/src/bin/joyful.rs new file mode 100644 index 0000000..5e26148 --- /dev/null +++ b/src/bin/joyful.rs @@ -0,0 +1,278 @@ +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)] +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, +} + +fn main() { + // Parse Command-line + let args = Args::parse(); + + // Parse configs + let config_files = get_config_files(args.config); + + // Initialize TTS + + // Create Virtual Devices + + // Create Physical Devices + + // Create Rules + + // Create listening threads? + + // 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 ( +// "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 +// }