package cmd
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/fsnotify/fsnotify"
"github.com/spf13/cobra"
"mercemay.top/src/tilstream/internal/devserver"
)
var (
serveAddr string
serveRoot string
serveWatch string
)
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Serve the built site locally with live reload",
RunE: runServe,
}
func init() {
serveCmd.Flags().StringVar(&serveAddr, "addr", ":1313", "listen address")
serveCmd.Flags().StringVar(&serveRoot, "root", "public", "directory to serve")
serveCmd.Flags().StringVar(&serveWatch, "watch", "public", "directory to watch for reload")
}
func runServe(cmd *cobra.Command, args []string) error {
PrintBanner()
cfg := devserver.DefaultConfig(serveRoot)
cfg.Addr = serveAddr
srv := devserver.New(cfg)
ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
defer cancel()
go watchAndReload(ctx, serveWatch, srv)
fmt.Fprintf(os.Stdout, "listening on %s (root=%s)\n", serveAddr, serveRoot)
return srv.Run(ctx)
}
func watchAndReload(ctx context.Context, dir string, srv *devserver.Server) {
w, err := fsnotify.NewWatcher()
if err != nil {
fmt.Fprintln(os.Stderr, "fsnotify:", err)
return
}
defer w.Close()
if err := w.Add(dir); err != nil {
fmt.Fprintln(os.Stderr, "watch:", err)
return
}
debounce := time.NewTimer(0)
<-debounce.C
for {
select {
case <-ctx.Done():
return
case ev, ok := <-w.Events:
if !ok {
return
}
if ev.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove|fsnotify.Rename) == 0 {
continue
}
debounce.Reset(150 * time.Millisecond)
case err := <-w.Errors:
if err != nil {
fmt.Fprintln(os.Stderr, "watch:", err)
}
case <-debounce.C:
srv.Reload()
}
}
}
// ServeAddr reports the configured listen address.
func ServeAddr() string { return serveAddr }