Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
55 changes: 44 additions & 11 deletions pkg/config/watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"fmt"
"path/filepath"
"sync"
"time"

Expand All @@ -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.
Expand All @@ -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
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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
}
}
}
Expand Down