From 80ad647336f8687bfd740c6eef9ad2a61433955d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 16 Apr 2026 12:36:39 +0530 Subject: [PATCH] Start work on config watcher kitten --- go.mod | 1 + go.sum | 2 + tools/cmd/tool/main.go | 1 + tools/config/api.go | 12 +++- tools/watch/api.go | 144 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 tools/watch/api.go diff --git a/go.mod b/go.mod index edcce3bab..1d9b48fd1 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/kovidgoyal/imaging v1.8.21 github.com/nwaples/rardecode/v2 v2.2.2 github.com/seancfoley/ipaddress-go v1.7.1 + github.com/sgtdi/fswatcher v1.2.0 github.com/shirou/gopsutil/v4 v4.26.3 github.com/ulikunitz/xz v0.5.15 github.com/zeebo/xxh3 v1.1.0 diff --git a/go.sum b/go.sum index c9503fa3c..ad59687ba 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/seancfoley/bintree v1.3.1 h1:cqmmQK7Jm4aw8gna0bP+huu5leVOgHGSJBEpUx3E github.com/seancfoley/bintree v1.3.1/go.mod h1:hIUabL8OFYyFVTQ6azeajbopogQc2l5C/hiXMcemWNU= github.com/seancfoley/ipaddress-go v1.7.1 h1:fDWryS+L8iaaH5RxIKbY0xB5Z+Zxk8xoXLN4S4eAPdQ= github.com/seancfoley/ipaddress-go v1.7.1/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw= +github.com/sgtdi/fswatcher v1.2.0 h1:uSJuMc3/Eo/vaPnZWpJ42EFYb5j38cZENmkszOV0yhw= +github.com/sgtdi/fswatcher v1.2.0/go.mod h1:smzXnaqu0SYJQNIwGLLkvRkpH4RdEACB7avMSsSaqjQ= github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= diff --git a/tools/cmd/tool/main.go b/tools/cmd/tool/main.go index 597fb5bea..aa875e4a0 100644 --- a/tools/cmd/tool/main.go +++ b/tools/cmd/tool/main.go @@ -134,6 +134,7 @@ func KittyToolEntryPoints(root *cli.Command) { return confirm_and_run_exe(args) }, }) + // __watch_conf__ // __convert_image__ images.ConvertEntryPoint(root) diff --git a/tools/config/api.go b/tools/config/api.go index 8e16c8646..089694bce 100644 --- a/tools/config/api.go +++ b/tools/config/api.go @@ -39,9 +39,10 @@ type ConfigLine struct { } type ConfigParser struct { - LineHandler func(key, val string) error - CommentsHandler func(line string) error - SourceHandler func(text, path string) + LineHandler func(key, val string) error + CommentsHandler func(line string) error + SourceHandler func(text, path string) + AllIncludedFiles *utils.Set[string] bad_lines []ConfigLine seen_includes map[string]bool @@ -111,11 +112,16 @@ func ExpandVars(x string) string { }) } +const OverridesFileName = "" + func (self *ConfigParser) parse(scanner Scanner, name, base_path_for_includes string, depth int) error { if self.seen_includes[name] { // avoid include loops return nil } self.seen_includes[name] = true + if self.AllIncludedFiles != nil && name != OverridesFileName { + self.AllIncludedFiles.Add(name) + } recurse := func(r io.Reader, nname, base_path_for_includes string) error { if depth > 32 { diff --git a/tools/watch/api.go b/tools/watch/api.go new file mode 100644 index 000000000..c5e46d9a0 --- /dev/null +++ b/tools/watch/api.go @@ -0,0 +1,144 @@ +package watch + +import ( + "bufio" + "context" + "fmt" + "os" + "path/filepath" + "strconv" + "sync" + "time" + + "github.com/sgtdi/fswatcher" + "golang.org/x/sys/unix" + + "github.com/kovidgoyal/kitty/tools/cli" + "github.com/kovidgoyal/kitty/tools/config" + "github.com/kovidgoyal/kitty/tools/utils" +) + +var _ = fmt.Print + +// watch_dir starts fswatcher in a background goroutine and pipes events to a custom channel. +func watch_dir(ctx context.Context, path string, debounce time.Duration, eventChan chan<- fswatcher.WatchEvent) error { + w, err := fswatcher.New( + fswatcher.WithPath(path), + fswatcher.WithCooldown(debounce), + ) + if err != nil { + return err + } + go w.Watch(ctx) + go func() { + for event := range w.Events() { + eventChan <- event + } + }() + return nil +} + +type config_file_collection struct { + mutex sync.Mutex + config_paths []string + dirs_to_watch []string +} + +func (cfc *config_file_collection) get_list_of_config_files() *utils.Set[string] { + cp := config.ConfigParser{ + AllIncludedFiles: utils.NewSet[string](), LineHandler: func(k, v string) error { return nil }} + cp.ParseFiles(cfc.config_paths...) + for _, path := range cfc.config_paths { + path = filepath.Clean(path) + cp.AllIncludedFiles.Add(path) + for _, q := range []string{"dark-theme.auto.conf", "light-theme.auto.conf", "no-preference-theme.auto.conf"} { + q = filepath.Join(filepath.Dir(path), q) + cp.AllIncludedFiles.Add(filepath.Clean(q)) + } + } + return cp.AllIncludedFiles +} + +func (cfc *config_file_collection) EventIsSignificant(ev fswatcher.WatchEvent) bool { + cfc.mutex.Lock() + defer cfc.mutex.Unlock() + conf_files := cfc.get_list_of_config_files() + q := filepath.Clean(ev.Path) + return conf_files.Has(q) +} + +func watch_for_kitty_config_changes(action func() error, debounce_time time.Duration, config_paths []string) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + event_chan := make(chan fswatcher.WatchEvent) + dirs := utils.NewSet[string](len(config_paths)) + for _, path := range config_paths { + if parent := filepath.Dir(path); parent != "" && parent != "." && parent != "/" { + dirs.Add(path) + } + } + if dirs.Len() == 0 { + return fmt.Errorf("No directories to watch provided") + } + cfc := config_file_collection{config_paths: config_paths, dirs_to_watch: dirs.AsSlice()} + + filtered_action := func(ev fswatcher.WatchEvent) error { + if cfc.EventIsSignificant(ev) { + return action() + } + return nil + } + for _, path := range cfc.dirs_to_watch { + if err := watch_dir(ctx, path, debounce_time, event_chan); err != nil { + return err + } + } + stdinClosed := make(chan struct{}) + go func() { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + } + close(stdinClosed) + }() + for { + select { + case event := <-event_chan: + if err := filtered_action(event); err != nil { + fmt.Fprintf(os.Stderr, "failed to signal kitty in event: %s with error: %s\n", event, err) + } + case <-stdinClosed: + return nil + } + } +} + +func signal_kitty_to_reload_config(kitty_pid int) error { + return unix.Kill(kitty_pid, unix.SIGUSR1) +} + +func EntryPoint(root *cli.Command) { + root.AddSubCommand(&cli.Command{ + Name: "__watch_conf__", + Hidden: true, + OnlyArgsAllowed: true, + Run: func(cmd *cli.Command, args []string) (rc int, err error) { + if len(args) < 3 { + return 1, fmt.Errorf("Usage: __watch_conf__ kitty_pid debounce_time_ms config_paths...") + } + kitty_pid, err := strconv.Atoi(args[0]) + if err != nil { + return 1, err + } + debounce_time_ms, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return 1, err + } + if err = watch_for_kitty_config_changes( + func() error { return signal_kitty_to_reload_config(kitty_pid) }, + time.Millisecond*time.Duration(debounce_time_ms), args[2:]); err != nil { + return 1, err + } + return 0, nil + }, + }) +}