package host import ( "os" "path/filepath" "sync" "time" ) // Watcher monitors files for changes and triggers callbacks. type Watcher struct { dir string extensions []string onChange func() interval time.Duration mu sync.Mutex modTimes map[string]time.Time stop chan struct{} wg sync.WaitGroup } // WatcherOption configures a Watcher. type WatcherOption func(*Watcher) // WithExtensions sets file extensions to watch (e.g., ".go", ".html"). func WithExtensions(exts ...string) WatcherOption { return func(w *Watcher) { w.extensions = exts } } // WithInterval sets the polling interval for file changes. func WithInterval(d time.Duration) WatcherOption { return func(w *Watcher) { w.interval = d } } // NewWatcher creates a new file watcher that monitors the specified directory. func NewWatcher(dir string, onChange func(), opts ...WatcherOption) *Watcher { w := &Watcher{ dir: dir, extensions: []string{".go"}, onChange: onChange, interval: 500 * time.Millisecond, modTimes: make(map[string]time.Time), stop: make(chan struct{}), } for _, opt := range opts { opt(w) } return w } // Start begins watching for file changes. func (w *Watcher) Start() error { // Initial scan to populate mod times if err := w.scan(false); err != nil { return err } w.wg.Add(1) go w.watch() return nil } // Stop stops the watcher. func (w *Watcher) Stop() { close(w.stop) w.wg.Wait() } func (w *Watcher) watch() { defer w.wg.Done() ticker := time.NewTicker(w.interval) defer ticker.Stop() for { select { case <-w.stop: return case <-ticker.C: if err := w.scan(true); err != nil { // Log error but continue watching continue } } } } func (w *Watcher) scan(notify bool) error { w.mu.Lock() defer w.mu.Unlock() changed := false seen := make(map[string]bool) err := filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error { if err != nil { return nil // Skip files we can't access } if info.IsDir() { // Skip hidden directories and vendor name := info.Name() if len(name) > 0 && name[0] == '.' { return filepath.SkipDir } if name == "vendor" || name == "node_modules" { return filepath.SkipDir } return nil } // Check if extension matches ext := filepath.Ext(path) if !w.matchesExtension(ext) { return nil } seen[path] = true modTime := info.ModTime() if prev, exists := w.modTimes[path]; !exists { // New file w.modTimes[path] = modTime if notify { changed = true } } else if !modTime.Equal(prev) { // Modified file w.modTimes[path] = modTime changed = true } return nil }) if err != nil { return err } // Check for deleted files for path := range w.modTimes { if !seen[path] { delete(w.modTimes, path) changed = true } } if changed && notify && w.onChange != nil { w.onChange() } return nil } func (w *Watcher) matchesExtension(ext string) bool { for _, e := range w.extensions { if e == ext { return true } } return false }