+107
-25
appview/pages/pages.go
+107
-25
appview/pages/pages.go
···
11
11
"io/fs"
12
12
"log"
13
13
"net/http"
14
+
"os"
14
15
"path"
15
16
"path/filepath"
16
17
"slices"
···
35
36
var Files embed.FS
36
37
37
38
type Pages struct {
38
-
t map[string]*template.Template
39
+
t map[string]*template.Template
40
+
dev bool
41
+
embedFS embed.FS
42
+
templateDir string // Path to templates on disk for dev mode
39
43
}
40
44
41
-
func NewPages() *Pages {
42
-
templates := make(map[string]*template.Template)
45
+
func NewPages(dev bool) *Pages {
46
+
p := &Pages{
47
+
t: make(map[string]*template.Template),
48
+
dev: dev,
49
+
embedFS: Files,
50
+
templateDir: "appview/pages",
51
+
}
43
52
53
+
// Initial load of all templates
54
+
p.loadAllTemplates()
55
+
56
+
return p
57
+
}
58
+
59
+
func (p *Pages) loadAllTemplates() {
60
+
templates := make(map[string]*template.Template)
44
61
var fragmentPaths []string
62
+
63
+
// Use embedded FS for initial loading
45
64
// First, collect all fragment paths
46
-
err := fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error {
65
+
err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
47
66
if err != nil {
48
67
return err
49
68
}
50
-
51
69
if d.IsDir() {
52
70
return nil
53
71
}
54
-
55
72
if !strings.HasSuffix(path, ".html") {
56
73
return nil
57
74
}
58
-
59
75
if !strings.Contains(path, "fragments/") {
60
76
return nil
61
77
}
62
-
63
78
name := strings.TrimPrefix(path, "templates/")
64
79
name = strings.TrimSuffix(name, ".html")
65
-
66
80
tmpl, err := template.New(name).
67
81
Funcs(funcMap()).
68
-
ParseFS(Files, path)
82
+
ParseFS(p.embedFS, path)
69
83
if err != nil {
70
84
log.Fatalf("setting up fragment: %v", err)
71
85
}
72
-
73
86
templates[name] = tmpl
74
87
fragmentPaths = append(fragmentPaths, path)
75
88
log.Printf("loaded fragment: %s", name)
···
80
93
}
81
94
82
95
// Then walk through and setup the rest of the templates
83
-
err = fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error {
96
+
err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
84
97
if err != nil {
85
98
return err
86
99
}
87
-
88
100
if d.IsDir() {
89
101
return nil
90
102
}
91
-
92
103
if !strings.HasSuffix(path, "html") {
93
104
return nil
94
105
}
95
-
96
106
// Skip fragments as they've already been loaded
97
107
if strings.Contains(path, "fragments/") {
98
108
return nil
99
109
}
100
-
101
110
// Skip layouts
102
111
if strings.Contains(path, "layouts/") {
103
112
return nil
104
113
}
105
-
106
114
name := strings.TrimPrefix(path, "templates/")
107
115
name = strings.TrimSuffix(name, ".html")
108
-
109
116
// Add the page template on top of the base
110
117
allPaths := []string{}
111
118
allPaths = append(allPaths, "templates/layouts/*.html")
···
113
120
allPaths = append(allPaths, path)
114
121
tmpl, err := template.New(name).
115
122
Funcs(funcMap()).
116
-
ParseFS(Files, allPaths...)
123
+
ParseFS(p.embedFS, allPaths...)
117
124
if err != nil {
118
125
return fmt.Errorf("setting up template: %w", err)
119
126
}
120
-
121
127
templates[name] = tmpl
122
128
log.Printf("loaded template: %s", name)
123
129
return nil
···
127
133
}
128
134
129
135
log.Printf("total templates loaded: %d", len(templates))
136
+
p.t = templates
137
+
}
130
138
131
-
return &Pages{
132
-
t: templates,
139
+
// loadTemplateFromDisk loads a template from the filesystem in dev mode
140
+
func (p *Pages) loadTemplateFromDisk(name string) error {
141
+
if !p.dev {
142
+
return nil
133
143
}
134
-
}
135
144
136
-
type LoginParams struct {
145
+
log.Printf("reloading template from disk: %s", name)
146
+
147
+
// Find all fragments first
148
+
var fragmentPaths []string
149
+
err := filepath.WalkDir(filepath.Join(p.templateDir, "templates"), func(path string, d fs.DirEntry, err error) error {
150
+
if err != nil {
151
+
return err
152
+
}
153
+
if d.IsDir() {
154
+
return nil
155
+
}
156
+
if !strings.HasSuffix(path, ".html") {
157
+
return nil
158
+
}
159
+
if !strings.Contains(path, "fragments/") {
160
+
return nil
161
+
}
162
+
fragmentPaths = append(fragmentPaths, path)
163
+
return nil
164
+
})
165
+
if err != nil {
166
+
return fmt.Errorf("walking disk template dir for fragments: %w", err)
167
+
}
168
+
169
+
// Find the template path on disk
170
+
templatePath := filepath.Join(p.templateDir, "templates", name+".html")
171
+
if _, err := os.Stat(templatePath); os.IsNotExist(err) {
172
+
return fmt.Errorf("template not found on disk: %s", name)
173
+
}
174
+
175
+
// Create a new template
176
+
tmpl := template.New(name).Funcs(funcMap())
177
+
178
+
// Parse layouts
179
+
layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html")
180
+
layouts, err := filepath.Glob(layoutGlob)
181
+
if err != nil {
182
+
return fmt.Errorf("finding layout templates: %w", err)
183
+
}
184
+
185
+
// Create paths for parsing
186
+
allFiles := append(layouts, fragmentPaths...)
187
+
allFiles = append(allFiles, templatePath)
188
+
189
+
// Parse all templates
190
+
tmpl, err = tmpl.ParseFiles(allFiles...)
191
+
if err != nil {
192
+
return fmt.Errorf("parsing template files: %w", err)
193
+
}
194
+
195
+
// Update the template in the map
196
+
p.t[name] = tmpl
197
+
log.Printf("template reloaded from disk: %s", name)
198
+
return nil
137
199
}
138
200
139
201
func (p *Pages) execute(name string, w io.Writer, params any) error {
140
-
return p.t[name].ExecuteTemplate(w, "layouts/base", params)
202
+
// In dev mode, reload the template from disk before executing
203
+
if p.dev {
204
+
if err := p.loadTemplateFromDisk(name); err != nil {
205
+
log.Printf("warning: failed to reload template %s from disk: %v", name, err)
206
+
// Continue with the existing template
207
+
}
208
+
}
209
+
210
+
tmpl, exists := p.t[name]
211
+
if !exists {
212
+
return fmt.Errorf("template not found: %s", name)
213
+
}
214
+
215
+
return tmpl.ExecuteTemplate(w, "layouts/base", params)
141
216
}
142
217
143
218
func (p *Pages) executePlain(name string, w io.Writer, params any) error {
···
146
221
147
222
func (p *Pages) executeRepo(name string, w io.Writer, params any) error {
148
223
return p.t[name].ExecuteTemplate(w, "layouts/repobase", params)
224
+
}
225
+
226
+
type LoginParams struct {
149
227
}
150
228
151
229
func (p *Pages) Login(w io.Writer, params LoginParams) error {
···
794
872
}
795
873
796
874
func (p *Pages) Static() http.Handler {
875
+
if p.dev {
876
+
return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
877
+
}
878
+
797
879
sub, err := fs.Sub(Files, "static")
798
880
if err != nil {
799
881
log.Fatalf("no static dir found? that's crazy: %v", err)
+1
-1
appview/state/state.go
+1
-1
appview/state/state.go
+10
-1
flake.nix
+10
-1
flake.nix
···
173
173
${pkgs.air}/bin/air -c /dev/null \
174
174
-build.cmd "${pkgs.tailwindcss}/bin/tailwindcss -i input.css -o ./appview/pages/static/tw.css && ${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \
175
175
-build.bin "./out/${name}.out" \
176
-
-build.include_ext "go,html,css"
176
+
-build.include_ext "go"
177
+
'';
178
+
tailwind-watcher =
179
+
pkgs.writeShellScriptBin "run"
180
+
''
181
+
${pkgs.tailwindcss}/bin/tailwindcss -w -i input.css -o ./appview/pages/static/tw.css
177
182
'';
178
183
in {
179
184
watch-appview = {
···
183
188
watch-knotserver = {
184
189
type = "app";
185
190
program = ''${air-watcher "knotserver"}/bin/run'';
191
+
};
192
+
watch-tailwind = {
193
+
type = "app";
194
+
program = ''${tailwind-watcher}/bin/run'';
186
195
};
187
196
});
188
197