Start work on config watcher kitten

This commit is contained in:
Kovid Goyal 2026-04-16 12:36:39 +05:30
parent a4608c77a6
commit 80ad647336
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
5 changed files with 157 additions and 3 deletions

1
go.mod
View file

@ -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

2
go.sum
View file

@ -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=

View file

@ -134,6 +134,7 @@ func KittyToolEntryPoints(root *cli.Command) {
return confirm_and_run_exe(args)
},
})
// __watch_conf__
// __convert_image__
images.ConvertEntryPoint(root)

View file

@ -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 = "<overrides>"
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 {

144
tools/watch/api.go Normal file
View file

@ -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
},
})
}