package main import ( "fmt" "log" "maps" "os" "path/filepath" "strings" "sync" "time" ) type FileWatcher struct { files map[string]time.Time mu sync.RWMutex restartCh chan bool stopCh chan bool debounceMs int lastEvent time.Time pollMs int } func NewFileWatcher(debounceMs int) (*FileWatcher, error) { return &FileWatcher{ files: make(map[string]time.Time), restartCh: make(chan bool, 1), stopCh: make(chan bool, 1), debounceMs: debounceMs, pollMs: 250, // Poll every 250ms }, nil } func (fw *FileWatcher) AddFile(path string) error { absPath, err := filepath.Abs(path) if err != nil { return err } info, err := os.Stat(absPath) if err != nil { return err } fw.mu.Lock() fw.files[absPath] = info.ModTime() fw.mu.Unlock() fmt.Printf("Watching: %s\n", absPath) return nil } func (fw *FileWatcher) AddDirectory(dir string) error { return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && strings.HasSuffix(path, ".lua") { return fw.AddFile(path) } return nil }) } func (fw *FileWatcher) Start() <-chan bool { go fw.pollLoop() return fw.restartCh } func (fw *FileWatcher) pollLoop() { ticker := time.NewTicker(time.Duration(fw.pollMs) * time.Millisecond) defer ticker.Stop() for { select { case <-fw.stopCh: return case <-ticker.C: fw.checkFiles() } } } func (fw *FileWatcher) checkFiles() { fw.mu.RLock() files := make(map[string]time.Time, len(fw.files)) maps.Copy(files, fw.files) fw.mu.RUnlock() changed := false for path, lastMod := range files { info, err := os.Stat(path) if err != nil { continue } if info.ModTime().After(lastMod) { fw.mu.Lock() fw.files[path] = info.ModTime() fw.mu.Unlock() now := time.Now() if now.Sub(fw.lastEvent) > time.Duration(fw.debounceMs)*time.Millisecond { fw.lastEvent = now log.Printf("File changed: %s", path) changed = true } } } if changed { select { case fw.restartCh <- true: default: } } } func (fw *FileWatcher) Close() error { select { case fw.stopCh <- true: default: } return nil } func (fw *FileWatcher) DiscoverRequiredFiles(scriptPath string) error { if err := fw.AddFile(scriptPath); err != nil { return err } scriptDir := filepath.Dir(scriptPath) return fw.AddDirectory(scriptDir) }