From 6d3673673eb62e5c35509727395afec7d5a02b3b Mon Sep 17 00:00:00 2001 From: blin Date: Wed, 25 Mar 2026 17:54:08 -0700 Subject: [PATCH] config: hot-reload config files when changed on disk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add StartConfigWatcher in internal/config/watch.go. It takes a slice of WatchedFile values, each with a Path func and an OnChange callback, and polls them once per second. When a file is modified or the resolved path changes the callback fires. Callbacks run on a background goroutine — callers dispatch to the main thread via timerChan. Wire up three watchers in cmd/micro/micro.go: - colorschemes/.micro → InitColorscheme + Redraw - settings.json → ReadSettings + InitGlobalSettings + Redraw - bindings.json → InitBindings This lets any external tool (theme managers, dotfile syncing, etc.) update micro's config files while the editor is open and have the changes applied immediately without a restart. No new dependencies. --- cmd/micro/micro.go | 45 ++++++++++++++++++++++++++++++ internal/config/watch.go | 59 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 internal/config/watch.go diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index c8a99b2517..07b4f77aed 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -441,6 +441,51 @@ func main() { screen.TermMessage(err) } + config.StartConfigWatcher([]config.WatchedFile{ + // Reload the colorscheme when the active .micro file changes on disk. + { + Path: func() string { + name := "default" + if cs, ok := config.GlobalSettings["colorscheme"].(string); ok { + name = cs + } + return filepath.Join(config.ConfigDir, "colorschemes", name+".micro") + }, + OnChange: func() { + timerChan <- func() { + if err := config.InitColorscheme(); err == nil { + screen.Redraw() + } + } + }, + }, + // Reload settings.json when it changes on disk. + { + Path: func() string { + return filepath.Join(config.ConfigDir, "settings.json") + }, + OnChange: func() { + timerChan <- func() { + if err := config.ReadSettings(); err == nil { + config.InitGlobalSettings() + screen.Redraw() + } + } + }, + }, + // Reload bindings.json when it changes on disk. + { + Path: func() string { + return filepath.Join(config.ConfigDir, "bindings.json") + }, + OnChange: func() { + timerChan <- func() { + action.InitBindings() + } + }, + }, + }) + if clipErr != nil { log.Println(clipErr, " or change 'clipboard' option") } diff --git a/internal/config/watch.go b/internal/config/watch.go new file mode 100644 index 0000000000..b510f173ac --- /dev/null +++ b/internal/config/watch.go @@ -0,0 +1,59 @@ +package config + +import ( + "os" + "time" +) + +// WatchedFile describes a file to watch and the callback to invoke on modification. +type WatchedFile struct { + Path func() string // resolves to the current absolute path each tick + OnChange func() // called when modified; runs on a background goroutine +} + +// StartConfigWatcher checks each WatchedFile once per second and calls its +// OnChange when the file is modified or the resolved path changes. +// OnChange runs on a background goroutine — post to a channel rather than +// mutating state directly. +// The returned function stops the watcher. +func StartConfigWatcher(files []WatchedFile) func() { + stop := make(chan struct{}) + + go func() { + type fileState struct { + path string + lastMod time.Time + } + states := make([]fileState, len(files)) + for i, f := range files { + p := f.Path() + states[i].path = p + if fi, err := os.Stat(p); err == nil { + states[i].lastMod = fi.ModTime() + } + } + + for { + time.Sleep(1 * time.Second) + select { + case <-stop: + return + default: + } + for i, f := range files { + newPath := f.Path() + fi, err := os.Stat(newPath) + if err != nil { + continue + } + if newPath != states[i].path || fi.ModTime().After(states[i].lastMod) { + states[i].path = newPath + states[i].lastMod = fi.ModTime() + f.OnChange() + } + } + } + }() + + return func() { close(stop) } +}