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)
+}