From e1940006d8d1386ec60a016f9399768d6accad06 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Tue, 15 Jul 2025 23:38:53 +0000 Subject: [PATCH] Support live re-loading of rules. (#2) Reviewed-on: https://git.annabunches.net/anna/joyful/pulls/2 Co-authored-by: Anna Rose Wiggins Co-committed-by: Anna Rose Wiggins --- cmd/joyful/main.go | 64 +++++++++++++------ cmd/joyful/threads.go | 57 ++++++++++++++++- cmd/joyful/types.go | 1 + .../mapping_rule_axis_to_relaxis.go | 5 -- 4 files changed, 102 insertions(+), 25 deletions(-) diff --git a/cmd/joyful/main.go b/cmd/joyful/main.go index f6b8e5f..6740085 100644 --- a/cmd/joyful/main.go +++ b/cmd/joyful/main.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "sync" "git.annabunches.net/annabunches/joyful/internal/config" "git.annabunches.net/annabunches/joyful/internal/logger" @@ -63,24 +64,7 @@ func main() { // Initialize physical devices pDevices := initPhysicalDevices(config) - // Initialize rules - rules := config.BuildRules(pDevices, getVirtualDevices(vBuffersByName)) - logger.Logf("Created %d mapping rules.", len(rules)) - - // start listening for events on devices and timers - eventChannel := make(chan ChannelEvent, 1000) - for _, device := range pDevices { - go eventWatcher(device, eventChannel) - } - - timerCount := 0 - for _, rule := range rules { - if timedRule, ok := rule.(mappingrules.TimedEventEmitter); ok { - go timerWatcher(timedRule, eventChannel) - timerCount++ - } - } - logger.Logf("registered %d timers", timerCount) + rules, eventChannel, doneChannel, wg := loadRules(config, pDevices, getVirtualDevices(vBuffersByName)) // initialize the mode variable mode := config.GetModes()[0] @@ -117,6 +101,50 @@ func main() { 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.") + doneChannel <- true + 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() // reload the config + rules, eventChannel, doneChannel, wg = loadRules(config, pDevices, getVirtualDevices(vBuffersByName)) } } } + +func loadRules( + config *config.ConfigParser, + pDevices map[string]*evdev.InputDevice, + vDevices map[string]*evdev.InputDevice) ([]mappingrules.MappingRule, <-chan ChannelEvent, chan bool, *sync.WaitGroup) { + + var wg sync.WaitGroup + eventChannel := make(chan ChannelEvent, 1000) + doneChannel := make(chan bool) + + // 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, doneChannel, &wg) + } + + timerCount := 0 + for _, rule := range rules { + if timedRule, ok := rule.(mappingrules.TimedEventEmitter); ok { + wg.Add(1) + go timerWatcher(timedRule, eventChannel, doneChannel, &wg) + timerCount++ + } + } + logger.Logf("registered %d timers", timerCount) + + go consoleWatcher(eventChannel, &wg) + + return rules, eventChannel, doneChannel, &wg +} diff --git a/cmd/joyful/threads.go b/cmd/joyful/threads.go index e630881..f722b42 100644 --- a/cmd/joyful/threads.go +++ b/cmd/joyful/threads.go @@ -1,6 +1,9 @@ package main import ( + "bufio" + "os" + "sync" "time" "git.annabunches.net/annabunches/joyful/internal/logger" @@ -13,8 +16,24 @@ const ( DeviceCheckIntervalMs = 1 ) -func eventWatcher(device *evdev.InputDevice, channel chan<- ChannelEvent) { +func eventWatcher( + device *evdev.InputDevice, + channel chan<- ChannelEvent, + done chan bool, + wg *sync.WaitGroup) { + + defer wg.Done() + for { + select { + case cancel := <-done: + if cancel { + done <- true + return + } + default: + } + event, err := device.ReadOne() if err != nil { logger.LogError(err, "Error while reading event. Disconnecting device.") @@ -28,8 +47,24 @@ func eventWatcher(device *evdev.InputDevice, channel chan<- ChannelEvent) { } } -func timerWatcher(rule mappingrules.TimedEventEmitter, channel chan<- ChannelEvent) { +func timerWatcher( + rule mappingrules.TimedEventEmitter, + channel chan<- ChannelEvent, + done chan bool, + wg *sync.WaitGroup) { + + defer wg.Done() + for { + select { + case cancel := <-done: + if cancel { + done <- true + return + } + default: + } + event := rule.TimerEvent() if event != nil { channel <- ChannelEvent{ @@ -41,3 +76,21 @@ func timerWatcher(rule mappingrules.TimedEventEmitter, channel chan<- ChannelEve time.Sleep(TimerCheckIntervalMs * time.Millisecond) } } + +// consoleWatcher reads input from stdin, and on receiving anything +func consoleWatcher(channel chan<- ChannelEvent, wg *sync.WaitGroup) { + defer wg.Done() + stdin := bufio.NewReader(os.Stdin) + for { + _, err := stdin.ReadString('\n') + if err != nil { + logger.LogErrorf(err, "Error in console input thread") + continue + } + + channel <- ChannelEvent{ + Type: ChannelEventReload, + } + return + } +} diff --git a/cmd/joyful/types.go b/cmd/joyful/types.go index 33cc015..57ea8fd 100644 --- a/cmd/joyful/types.go +++ b/cmd/joyful/types.go @@ -7,6 +7,7 @@ type ChannelEventType int const ( ChannelEventInput ChannelEventType = iota ChannelEventTimer + ChannelEventReload ) type ChannelEvent struct { diff --git a/internal/mappingrules/mapping_rule_axis_to_relaxis.go b/internal/mappingrules/mapping_rule_axis_to_relaxis.go index 731d067..16c3912 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/logger" "github.com/holoplot/go-evdev" "github.com/jonboulle/clockwork" ) @@ -53,10 +52,6 @@ func (rule *MappingRuleAxisToRelaxis) MatchEvent( return nil, nil } - defer func() { - logger.Logf("DEBUG: Rule '%s' nextEvent == '%v' with device value '%d'", rule.Name, rule.nextEvent, event.Value) - }() - // If we're inside the deadzone, unset the next event if rule.Input.InDeadZone(event.Value) { rule.nextEvent = NoNextEvent