Repos / s4g / 1089970831
commit 1089970831f543b407d307e787e674950085af6b
Author: Nhân <hi@imnhan.com>
Date:   Fri Jul 7 17:10:35 2023 +0700

    watch for changes in web dir

diff --git a/Makefile b/Makefile
index 19126c2..83c5cf1 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@ build:
 	go build -o dist/
 
 watch:
-	find . -name '*.go' -or -name '*.js' -or -name '*.tmpl' -or -name '*.dj' \
+	find . -name '*.go' -or -name '*.js' \
 	| entr -rc go run .
 
 # Cheating a little because the djot.js repo on github does not provide builds
diff --git a/go.mod b/go.mod
index 829ab3e..432d81d 100644
--- a/go.mod
+++ b/go.mod
@@ -4,4 +4,9 @@ go 1.20
 
 require github.com/BurntSushi/toml v1.3.2
 
-require golang.org/x/tools v0.10.0
+require (
+	github.com/fsnotify/fsnotify v1.6.0
+	golang.org/x/tools v0.10.0
+)
+
+require golang.org/x/sys v0.9.0 // indirect
diff --git a/go.sum b/go.sum
index d4d0018..27f5e0e 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,9 @@
 github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
+golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
 golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
diff --git a/main.go b/main.go
index db6a151..ba6daf7 100644
--- a/main.go
+++ b/main.go
@@ -17,8 +17,9 @@
 )
 
 const DJOT_EXT = ".dj"
+const SITE_EXT = ".wbmkr2k"
+const SITE_FILENAME = "website" + SITE_EXT
 const FEED_PATH = "feed.xml"
-const SITE_FILENAME = "website.wbmkr2k"
 
 func main() {
 	var port, folder string
@@ -32,8 +33,31 @@ func main() {
 	}
 
 	fsys := WriteDirFS(absolutePath)
-	site := readSiteMetadata(fsys)
 
+	regenerate(fsys)
+
+	// TODO: only rebuild necessary bits instead of regenerating
+	// the whole thing. To do that I'll probably need to:
+	// - Devise some sort of dependency graph
+	// - Filter out relevant FS events: this seems daunting considering the
+	// differences between OSes and applications (e.g. vim writes to temp file
+	// then renames)
+	closeWatcher := WatchLocalFS(fsys, func() {
+		fmt.Println("Change detected. Regenerating...")
+		regenerate(fsys)
+	})
+	defer closeWatcher()
+
+	println("Serving local website at http://localhost:" + port)
+	http.Handle("/", http.FileServer(http.FS(fsys)))
+	err = http.ListenAndServe("127.0.0.1:"+port, nil)
+	if err != nil {
+		panic(err)
+	}
+}
+
+func regenerate(fsys WritableFS) {
+	site := readSiteMetadata(fsys)
 	articles := findArticles(fsys)
 
 	// Sort articles, newest first
@@ -74,13 +98,6 @@ func main() {
 		FEED_PATH,
 		generateFeed(site, articlesInFeed, site.HomePath+FEED_PATH),
 	)
-
-	println("Serving local website at http://localhost:" + port)
-	http.Handle("/", http.FileServer(http.FS(fsys)))
-	err = http.ListenAndServe("127.0.0.1:"+port, nil)
-	if err != nil {
-		panic(err)
-	}
 }
 
 type SiteMetadata struct {
@@ -236,7 +253,7 @@ func findArticles(fsys WritableFS) (result []Article) {
 		}
 		_, err = toml.Decode(metaText, &meta)
 		if err != nil {
-			fmt.Printf("FIXME: Malformed article metadata in %s: %s", path, err)
+			fmt.Printf("FIXME: Malformed article metadata in %s: %s\n", path, err)
 			return nil
 		}
 
diff --git a/watcher.go b/watcher.go
new file mode 100644
index 0000000..33acb06
--- /dev/null
+++ b/watcher.go
@@ -0,0 +1,111 @@
+package main
+
+import (
+	"fmt"
+	"io/fs"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/fsnotify/fsnotify"
+)
+
+var WATCHED_EXTS = []string{DJOT_EXT, SITE_EXT, ".tmpl"}
+
+const debounceInterval = 500 * time.Millisecond
+
+// Watches for relevant changes in FS, debounces by debounceInterval,
+// then executes callback.
+// Returns cleanup function.
+func WatchLocalFS(fsys WritableFS, callback func()) (Close func() error) {
+	watcher, err := fsnotify.NewWatcher()
+	if err != nil {
+		panic(err)
+	}
+
+	fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
+		if !d.IsDir() {
+			return nil
+		}
+
+		fullPath := filepath.Join(fsys.Path(), path)
+
+		err = watcher.Add(fullPath)
+		if err != nil {
+			panic(err)
+		}
+
+		return nil
+	})
+
+	//printWatchList(watcher)
+
+	// Start listening for events.
+	events := make(chan struct{})
+	go func() {
+		for {
+			select {
+			case event, ok := <-watcher.Events:
+				if !ok {
+					return
+				}
+
+				// Avoid infinite loop
+				if event.Has(fsnotify.Write) &&
+					!contains(WATCHED_EXTS, filepath.Ext(event.Name)) {
+					break
+				}
+
+				//fmt.Println("EVENT:", event.Op, event.Name)
+
+				// Dynamically watch new/renamed folders
+				if event.Has(fsnotify.Create) || event.Has(fsnotify.Rename) {
+					stat, err := os.Stat(event.Name)
+					if err == nil && stat.IsDir() {
+						watcher.Add(event.Name)
+					}
+				}
+
+				events <- struct{}{}
+
+			case err, ok := <-watcher.Errors:
+				if !ok {
+					return
+				}
+				fmt.Println("error:", err)
+			}
+		}
+	}()
+
+	// Debounce
+	go func() {
+		timer := time.NewTimer(debounceInterval)
+		<-timer.C // drain once so callback isn't executed on startup
+		for {
+			select {
+			case <-events:
+				timer.Reset(debounceInterval)
+			case <-timer.C:
+				callback()
+			}
+		}
+	}()
+
+	return watcher.Close
+}
+
+func printWatchList(w *fsnotify.Watcher) {
+	fmt.Println("WatchList:")
+	for _, path := range w.WatchList() {
+		fmt.Println("  " + path)
+	}
+}
+
+func contains(s []string, e string) bool {
+	for _, a := range s {
+		if a == e {
+			return true
+		}
+	}
+	return false
+}
diff --git a/writablefs.go b/writablefs.go
index bf1624e..3930b1b 100644
--- a/writablefs.go
+++ b/writablefs.go
@@ -9,6 +9,7 @@
 type WritableFS interface {
 	fs.FS
 	WriteFile(path string, content []byte) error
+	Path() string
 }
 
 // Like os.DirFS but is writable
@@ -26,3 +27,7 @@ func (w writeDirFS) WriteFile(path string, content []byte) error {
 	fullPath := filepath.Join(string(w), path)
 	return os.WriteFile(fullPath, content, 0644)
 }
+
+func (w writeDirFS) Path() string {
+	return string(w)
+}