summaryrefslogtreecommitdiff
path: root/cmd/serve.go
blob: 634fcb6030b7d709328ee7e021a8faa6d136264e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package cmd

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"github.com/HimanshuSardana/kite/internal/build"
	"github.com/HimanshuSardana/kite/pkg/config"
	"github.com/fsnotify/fsnotify"
)

func runServe(args []string) {
	themeName := DefaultTheme
	port := DefaultPort

	if cfg, err := config.Load("config.yaml"); err == nil && cfg.DefaultTheme != "" {
		themeName = cfg.DefaultTheme
	}

	for i := 2; i < len(args); i++ {
		if args[i] == "--port" && i+1 < len(args) {
			port = args[i+1]
		}
		if args[i] != "--port" && args[i] != "--help" && args[i] != "-h" {
			themeName = args[i]
		}
	}

	// Initial build
	buildSite(themeName)

	// Start file watcher for hot-reload
	go watchAndRebuild(themeName)

	fs := http.FileServer(http.Dir("./output/"))
	http.Handle("/", fs)

	log.Printf("Serving on http://localhost:%s (with hot-reload)", port)

	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatalf("Server error: %s\n", err)
	}
}

func buildSite(themeName string) {
	log.Println("Building site...")

	themeCSS := fmt.Sprintf("./themes/%s/style.css", themeName)
	outputCSS := "./output/style.css"

	if err := copyFile(themeCSS, outputCSS); err != nil {
		log.Printf("Warning: Could not copy theme CSS: %v", err)
	}

	opts := build.BuildOptions{
		ThemeName: themeName,
	}

	if err := build.Build(opts); err != nil {
		log.Printf("Build error: %v", err)
	} else {
		log.Println("Build completed successfully!")
	}
}

func watchAndRebuild(themeName string) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Printf("Failed to create watcher: %v", err)
		return
	}
	defer watcher.Close()

	contentDir := "./content"
	if err := addDirRecursive(watcher, contentDir); err != nil {
		log.Printf("Failed to watch content directory: %v", err)
		return
	}

	// Also watch themes directory for theme changes
	themesDir := "./themes"
	if err := addDirRecursive(watcher, themesDir); err != nil {
		log.Printf("Warning: Could not watch themes directory: %v", err)
	}

	if err := watcher.Add("./config.yaml"); err != nil {
		log.Printf("Warning: Could not watch config.yaml: %v", err)
	}

	log.Println("Watching for changes in content/, themes/, and config.yaml...")

	var debounceTimer *time.Timer

	for {
		select {
		case event, ok := <-watcher.Events:
			if !ok {
				return
			}
			// Only trigger on write or create events
			if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
				log.Printf("Detected change: %s", event.Name)

				// If a new directory is created, watch it
				if event.Op&fsnotify.Create == fsnotify.Create {
					if info, err := os.Stat(event.Name); err == nil && info.IsDir() {
						addDirRecursive(watcher, event.Name)
					}
				}

				// Debounce: wait 500ms before rebuilding
				if debounceTimer != nil {
					debounceTimer.Stop()
				}
				debounceTimer = time.AfterFunc(500*time.Millisecond, func() {
					buildSite(themeName)
				})
			}
		case err, ok := <-watcher.Errors:
			if !ok {
				return
			}
			log.Printf("Watcher error: %v", err)
		}
	}
}

func addDirRecursive(watcher *fsnotify.Watcher, dir string) error {
	return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			log.Printf("Watching directory: %s", path)
			return watcher.Add(path)
		}
		return nil
	})
}

func copyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()

	if err := os.MkdirAll(filepath.Dir(dst), os.ModePerm); err != nil {
		return err
	}

	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, in)
	return err
}