From 08d5617aafcc966454a5c85f4b86d49b14b31ef9 Mon Sep 17 00:00:00 2001 From: BluePhi09 Date: Tue, 7 Jul 2026 00:39:05 +0200 Subject: [PATCH] fix(daemon): apply config written after daemon startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a fresh home the shell (Ambxst) starts the axctl daemon immediately but writes axctl.toml only moments later. The daemon stat'ed the config at startup and, when missing, skipped BOTH the initial apply AND the fsnotify watcher — so the first config ever written was silently ignored for the whole session. Result: on the very first login the generated compositor config (keybinds, appearance, layer rules) never materialized; everything started working only from the second login on. Fix: - always start the config watcher, even when the file doesn't exist yet (and create its parent directory so it can be watched) - watch parent directories in addition to the files themselves, because fsnotify cannot watch a nonexistent path (and file-level watches break on atomic rename saves) - filter events by path so unrelated files in the same directory (e.g. the generated hyprland.conf/.lua the daemon itself writes there) don't trigger spurious reloads Co-Authored-By: Claude Fable 5 --- main.go | 36 +++++++++++++++++----------- pkg/config/watcher.go | 55 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/main.go b/main.go index e1afa4c..0b18718 100644 --- a/main.go +++ b/main.go @@ -260,22 +260,30 @@ func runDaemon(customConfigPath string) { fmt.Printf("[axctl-config] Error applying config: %v\n", applyErr) } } - - // Watch for config changes - watcher, watchErr := config.NewConfigWatcher() - if watchErr != nil { - fmt.Printf("[axctl-config] Warning: could not start watcher: %v\n", watchErr) - } else { - watcher.Start(configPath, func(newCfg *config.TOMLConfig) { - fmt.Println("[axctl-config] Config changed, reloading...") - if applyErr := config.ApplyConfig(newCfg, comp); applyErr != nil { - fmt.Printf("[axctl-config] Error applying config: %v\n", applyErr) - } - }) - cfgWatcher = watcher + } else { + // On a fresh home the shell writes the config only after the daemon + // has started. Make sure the parent directory exists so the watcher + // below can pick the file up the moment it appears. + fmt.Printf("[axctl-config] No config file at %s yet, waiting for it to appear\n", configPath) + if mkErr := os.MkdirAll(filepath.Dir(configPath), 0755); mkErr != nil { + fmt.Printf("[axctl-config] Warning: could not create config dir: %v\n", mkErr) } + } + + // Watch for config changes — started even when the file doesn't exist + // yet, so the first config ever written is applied immediately instead + // of being ignored until the next session. + watcher, watchErr := config.NewConfigWatcher() + if watchErr != nil { + fmt.Printf("[axctl-config] Warning: could not start watcher: %v\n", watchErr) } else { - fmt.Printf("[axctl-config] No config file at %s, skipping\n", configPath) + watcher.Start(configPath, func(newCfg *config.TOMLConfig) { + fmt.Println("[axctl-config] Config changed, reloading...") + if applyErr := config.ApplyConfig(newCfg, comp); applyErr != nil { + fmt.Printf("[axctl-config] Error applying config: %v\n", applyErr) + } + }) + cfgWatcher = watcher } sig := make(chan os.Signal, 1) diff --git a/pkg/config/watcher.go b/pkg/config/watcher.go index a300f5a..73d9d3b 100644 --- a/pkg/config/watcher.go +++ b/pkg/config/watcher.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "path/filepath" "sync" "time" @@ -10,12 +11,13 @@ import ( // ConfigWatcher watches config files for changes and triggers reloads. type ConfigWatcher struct { - watcher *fsnotify.Watcher - configPath string - callback func(*TOMLConfig) - watched map[string]bool - mu sync.Mutex - done chan struct{} + watcher *fsnotify.Watcher + configPath string + callback func(*TOMLConfig) + watched map[string]bool + watchedDirs map[string]bool + mu sync.Mutex + done chan struct{} } // NewConfigWatcher creates a new config file watcher. @@ -25,9 +27,10 @@ func NewConfigWatcher() (*ConfigWatcher, error) { return nil, fmt.Errorf("creating fsnotify watcher: %w", err) } return &ConfigWatcher{ - watcher: w, - watched: make(map[string]bool), - done: make(chan struct{}), + watcher: w, + watched: make(map[string]bool), + watchedDirs: make(map[string]bool), + done: make(chan struct{}), }, nil } @@ -68,6 +71,13 @@ func (cw *ConfigWatcher) loop() { continue } + // Parent directories are watched too (to catch config files that + // don't exist yet), so filter out events for unrelated files + // living in the same directory. + if !cw.isConfigPath(event.Name) { + continue + } + // Debounce: reset timer on each event if debounceTimer != nil { debounceTimer.Stop() @@ -102,6 +112,14 @@ func (cw *ConfigWatcher) reload() { } } +// isConfigPath reports whether an fsnotify event path refers to one of the +// watched config files (as opposed to an unrelated file in a watched dir). +func (cw *ConfigWatcher) isConfigPath(name string) bool { + cw.mu.Lock() + defer cw.mu.Unlock() + return cw.watched[filepath.Clean(name)] +} + func (cw *ConfigWatcher) updateWatchedFiles() { cw.mu.Lock() defer cw.mu.Unlock() @@ -110,11 +128,26 @@ func (cw *ConfigWatcher) updateWatchedFiles() { newWatched := make(map[string]bool) for _, p := range paths { + p = filepath.Clean(p) newWatched[p] = true if !cw.watched[p] { if err := cw.watcher.Add(p); err != nil { - // File might not exist yet — not an error - fmt.Printf("[axctl-config] Warning: cannot watch %s: %v\n", p, err) + // File might not exist yet — the parent-directory watch + // below picks up its creation. + fmt.Printf("[axctl-config] Note: cannot watch %s yet: %v\n", p, err) + } + } + + // Also watch the parent directory: fsnotify cannot watch a path that + // does not exist yet, and file-level watches break when editors save + // via rename. The event loop filters events back down to the config + // paths, so unrelated files in the directory don't trigger reloads. + dir := filepath.Dir(p) + if !cw.watchedDirs[dir] { + if err := cw.watcher.Add(dir); err != nil { + fmt.Printf("[axctl-config] Warning: cannot watch dir %s: %v\n", dir, err) + } else { + cw.watchedDirs[dir] = true } } }