From 3bceab7105749656edee7956bfd7cae1f8dcdd3e Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Mon, 30 Oct 2023 11:20:55 +0100
Subject: [PATCH] feat: Support for tags #7

---
 dedup.go           | 90 ----------------------------------------------
 lighthouse.go      | 74 +++++++++++++++++++++++++++++++++++++-
 lighthouse_test.go | 54 ++++++++++++++++++++++++++--
 3 files changed, 125 insertions(+), 93 deletions(-)
 delete mode 100644 dedup.go

diff --git a/dedup.go b/dedup.go
deleted file mode 100644
index b53b8ff..0000000
--- a/dedup.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package watch
-
-//
-//// dedup is a function that watches a set of paths and prints a message when
-//func (w *Watch) Decouple(waitFor time.Duration) {
-//	if waitFor == 0 {
-//		waitFor = 100 * time.Millisecond
-//	}
-//	go decoupleLoop(w.watcher, waitFor)
-//	<-make(chan struct{}) // Block forever
-//}
-//
-//// dedupLoop is the main loop for dedup.
-//func decoupleLoop(w *fsnotify.Watcher, waitFor time.Duration) {
-//	var (
-//		mu     sync.Mutex
-//		timers = make(map[string]*time.Timer)
-//
-//		printEvent = func(e fsnotify.Event) {
-//			fmt.Printf("%s: %s\n", e.Op, e.Name)
-//
-//			// Don't need to remove the timer if you don't have a lot of files.
-//			mu.Lock()
-//			delete(timers, e.Name)
-//			mu.Unlock()
-//		}
-//	)
-//
-//	for {
-//		select {
-//		// Read from Errors.
-//		case err, ok := <-w.Errors:
-//			if !ok { // Channel was closed (i.e. Watcher.Close() was called).
-//				return
-//			}
-//			fmt.Println("error:", err)
-//		// Read from Events.
-//		case e, ok := <-w.Events:
-//			if !ok { // Channel was closed (i.e. Watcher.Close() was called).
-//				return
-//			}
-//
-//			if e.Op&fsnotify.Chmod == fsnotify.Chmod {
-//				//util.Remove(fullPath)
-//			}
-//
-//			if e.Op&fsnotify.Write == fsnotify.Write {
-//				//util.Remove(fullPath)
-//			}
-//
-//			if e.Op&fsnotify.Create == fsnotify.Create {
-//				//logging.LogInfo("watching: created file: %s", d)
-//				//scanDirectory(fullPath, "")
-//				//navigation.AddFile(fullPath)
-//			}
-//
-//			if e.Op&fsnotify.Remove == fsnotify.Remove {
-//				//logging.LogInfo("watching: removed file: %s", d)
-//				//removeFromWatchlist(fullPath)
-//				//util.Remove(fullPath)
-//				//navigation.RemoveFile(fullPath)
-//			}
-//
-//			if e.Op&fsnotify.Rename == fsnotify.Rename {
-//				//logging.LogInfo("watching: renamed file: %s", event.Name)
-//				//removeFromWatchlist(fullPath)
-//				//util.Remove(fullPath)
-//				//navigation.RemoveFile(fullPath)
-//			}
-//
-//			// Get timer.
-//			mu.Lock()
-//			t, ok := timers[e.Name]
-//			mu.Unlock()
-//
-//			// No timer yet, so create one.
-//			if !ok {
-//				t = time.AfterFunc(math.MaxInt64, func() { printEvent(e) })
-//				t.Stop()
-//
-//				mu.Lock()
-//				timers[e.Name] = t
-//				mu.Unlock()
-//			}
-//
-//			// Reset the timer for this path, so it will start from 100ms again.
-//			t.Reset(waitFor)
-//		}
-//	}
-//}
diff --git a/lighthouse.go b/lighthouse.go
index 723cbea..a2b503c 100644
--- a/lighthouse.go
+++ b/lighthouse.go
@@ -15,6 +15,7 @@ type Watch struct {
 	OnChange EventCallback
 	OnDelete EventCallback
 	OnRename EventCallback
+	Tags     []string
 }
 
 type EventErrorCallback func(err error)
@@ -64,6 +65,10 @@ type Lighthouse interface {
 	Sync() error
 
 	IsInSync() bool
+
+	WatchListByTags(tags []string) []string
+
+	RemoveByTags(tags []string) error
 }
 
 // NewLighthouse creates a new lighthouse instance.
@@ -121,6 +126,33 @@ func (l *lighthouse) WatchList() []string {
 
 }
 
+// WatchListByTags returns a list of all watched paths with the given tags.
+func (l *lighthouse) WatchListByTags(tags []string) []string {
+
+	if len(tags) == 0 {
+		return l.WatchList()
+	}
+
+	l.mutex.Lock()
+	defer l.mutex.Unlock()
+	var list []string
+	for k := range l.watchers {
+
+		if len(l.watchers[k].Tags) == 0 {
+			continue
+		}
+
+		for _, tag := range tags {
+			if tag == k {
+				list = append(list, k)
+				break
+			}
+		}
+	}
+	return list
+
+}
+
 // IsInSync returns true if all paths are watched by fsnotify.
 func (l *lighthouse) IsInSync() bool {
 	l.mutex.Lock()
@@ -238,6 +270,46 @@ func (l *lighthouse) Add(watch *Watch) error {
 	return nil
 }
 
+func (l *lighthouse) RemoveByTags(tags []string) error {
+
+	if len(tags) == 0 {
+		return nil
+	}
+
+	l.mutex.Lock()
+	defer l.mutex.Unlock()
+
+	var errReturn error
+
+	for _, tag := range tags {
+
+		for k := range l.watchers {
+
+			if len(l.watchers[k].Tags) == 0 {
+				continue
+			}
+
+			for _, t := range l.watchers[k].Tags {
+				if t == tag {
+					err := l.fsnotify.Remove(k)
+					if err != nil {
+						if errReturn == nil {
+							errReturn = err
+						} else {
+							errReturn = fmt.Errorf("%w, %v", errReturn, err)
+						}
+					}
+					delete(l.watchers, k)
+					break
+				}
+			}
+		}
+	}
+
+	return errReturn
+
+}
+
 // Remove removes a path from the watcher. If the path is not being watched or
 // the watcher is not active, an error is returned.
 func (l *lighthouse) Remove(path string) error {
@@ -254,9 +326,9 @@ func (l *lighthouse) Remove(path string) error {
 		return UnwatchedPathError{UnwatchedPath: path}
 	}
 
+	err := l.fsnotify.Remove(path)
 	delete(l.watchers, path)
 
-	err := l.fsnotify.Remove(path)
 	if err != nil {
 		return err
 	}
diff --git a/lighthouse_test.go b/lighthouse_test.go
index f9118fd..a62edbc 100644
--- a/lighthouse_test.go
+++ b/lighthouse_test.go
@@ -2,12 +2,62 @@ package watch
 
 import (
 	"errors"
+	"github.com/stretchr/testify/assert"
 	"os"
 	"testing"
-
-	"github.com/stretchr/testify/assert"
+	"time"
 )
 
+func TestRemoveByTagsAndWatchListByTags(t *testing.T) {
+	l := NewLighthouse()
+	l.SetDebounce(500 * time.Millisecond)
+
+	tmpDir1 := t.TempDir()
+	tmpDir2 := t.TempDir()
+	tmpDir3 := t.TempDir()
+
+	// Add a few watches
+	err := l.Add(&Watch{Path: tmpDir1, Tags: []string{"tag1", "tag2"}})
+	if err != nil {
+		t.Errorf("Failed to add watch: %v", err)
+	}
+
+	err = l.Add(&Watch{Path: tmpDir2, Tags: []string{"tag1"}})
+	if err != nil {
+		t.Errorf("Failed to add watch: %v", err)
+	}
+
+	err = l.Add(&Watch{Path: tmpDir3, Tags: []string{"tag2"}})
+	if err != nil {
+		t.Errorf("Failed to add watch: %v", err)
+	}
+
+	// Test WatchListByTags
+	list := l.WatchListByTags([]string{"tag1"})
+	expectedList := []string{tmpDir1, tmpDir2}
+	for i, path := range list {
+		if path != expectedList[i] {
+			t.Errorf("Expected %s, got %s", expectedList[i], path)
+		}
+	}
+
+	// Test RemoveByTags
+	err = l.RemoveByTags([]string{"tag1"})
+	if err != nil {
+		t.Errorf("Failed to remove by tags: %v", err)
+	}
+
+	// Confirm removal
+	if l.IsWatched(tmpDir1) || l.IsWatched(tmpDir2) {
+		t.Errorf("RemoveByTags did not remove the watches correctly")
+	}
+
+	// Confirm remaining watch
+	if !l.IsWatched(tmpDir3) {
+		t.Errorf("RemoveByTags removed an unrelated watch")
+	}
+}
+
 func TestAddWatch(t *testing.T) {
 	tempDir, err := os.MkdirTemp("", "watchtest")
 	assert.Nil(t, err)
-- 
GitLab