cmd/tilstream/cmd/serve.go

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 }