summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-27 08:40:32 +0300
committerPaul Buetow <paul@buetow.org>2026-04-27 08:40:32 +0300
commitad0cc92e16aa98ee10bead846c0ff4c74c5244d6 (patch)
treec405475e9744d0568b6ab3be96ba38021c731771
parent9ce439a217a8f2f5c48317bb5fb37e65d2f11ede (diff)
generator: lazy-init themeSet and navDefs with sync.Once
-rw-r--r--internal/generator/doc.go2
-rw-r--r--internal/generator/generator.go2
-rw-r--r--internal/generator/generator_test.go12
-rw-r--r--internal/generator/shared.go23
-rw-r--r--internal/generator/templates.go2
-rw-r--r--internal/generator/themes.go42
6 files changed, 49 insertions, 34 deletions
diff --git a/internal/generator/doc.go b/internal/generator/doc.go
index be82bdc..8eaf4db 100644
--- a/internal/generator/doc.go
+++ b/internal/generator/doc.go
@@ -8,7 +8,7 @@
// shared.css/shared.js and per-theme assets, then call atom.Generate.
// - themes.go — ListThemes / validThemeName helpers backed by the embedded FS.
// - shared.go — navDefs: shared {{define}} blocks (splashGate, navhints,
-// navmodal) merged at parse time with shell.tmpl so a single html/template
+// navSharedCSS) merged at parse time with shell.tmpl so a single html/template
// parse sees every name.
// - theme_sounds.go — Per-theme Web Audio parameters; one file is written per
// theme to dist/themes/<name>/sounds.json and the default theme's preset is
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 0b51edd..db9531b 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -99,7 +99,7 @@ func Run(cfg *config.Config) error {
if err != nil {
return fmt.Errorf("load shell template: %w", err)
}
- combined := shellSrc + "\n" + navDefs
+ combined := shellSrc + "\n" + getNavDefs()
tmpl, err := template.New("page").Parse(combined)
if err != nil {
return fmt.Errorf("parse page template: %w", err)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 0e3708b..56542a3 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -93,7 +93,7 @@ func TestJSONStringOrNull(t *testing.T) {
func TestThemeSoundPresetsMatchRegistry(t *testing.T) {
t.Parallel()
- for name := range themeSet {
+ for name := range getThemeSet() {
if _, ok := themeSoundPresets[name]; !ok {
t.Errorf("theme %q has no sound preset in themeSoundPresets", name)
}
@@ -112,7 +112,7 @@ func TestThemeSoundsJSON_ambientSchema(t *testing.T) {
t.Parallel()
// Verify the ambient schema is present and valid for every registered theme.
- for name := range themeSet {
+ for name := range getThemeSet() {
j := themeSoundsJSON(name)
if len(j) < 50 {
t.Fatalf("themeSoundsJSON(%q) too short: %q", name, j)
@@ -148,7 +148,7 @@ func TestThemeSoundsJSON_ambientSchema(t *testing.T) {
func TestThemeSoundPresetsAmbientPopulated(t *testing.T) {
t.Parallel()
- for name := range themeSet {
+ for name := range getThemeSet() {
preset, ok := themeSoundPresets[name]
if !ok {
t.Errorf("theme %q missing from themeSoundPresets", name)
@@ -170,7 +170,7 @@ func TestThemeSoundPresetsAmbientPopulated(t *testing.T) {
func TestThemeSoundPresetsAmbientValuesBounded(t *testing.T) {
t.Parallel()
- for name := range themeSet {
+ for name := range getThemeSet() {
preset, ok := themeSoundPresets[name]
if !ok {
continue
@@ -384,8 +384,8 @@ func TestLoadThemeMeta_neonHasFields(t *testing.T) {
func TestListThemes_sortedAndComplete(t *testing.T) {
t.Parallel()
names := ListThemes()
- if len(names) != len(themeSet) {
- t.Fatalf("len=%d, want %d", len(names), len(themeSet))
+ if len(names) != len(getThemeSet()) {
+ t.Fatalf("len=%d, want %d", len(names), len(getThemeSet()))
}
for i := 1; i < len(names); i++ {
if names[i] <= names[i-1] {
diff --git a/internal/generator/shared.go b/internal/generator/shared.go
index d0badb8..378a01f 100644
--- a/internal/generator/shared.go
+++ b/internal/generator/shared.go
@@ -2,6 +2,7 @@ package generator
import (
"log"
+ "sync"
"codeberg.org/snonux/snonux/internal/generator/templates"
)
@@ -18,13 +19,19 @@ import (
// Each theme ends its <style> with {{template "navSharedCSSInner"}} then calls
// {{template "splashGate"}}, {{template "navhints" .}}, {{template "navmodal" .}},
// and {{template "navscript" .}} at the appropriate points in its HTML.
-var navDefs = loadNavDefs()
+var (
+ navDefsCache string
+ navDefsOnce sync.Once
+)
-func loadNavDefs() string {
- s, err := templates.Shared("nav")
- if err != nil {
- log.Printf("warning: could not load shared nav template: %v", err)
- return ""
- }
- return s
+func getNavDefs() string {
+ navDefsOnce.Do(func() {
+ s, err := templates.Shared("nav")
+ if err != nil {
+ log.Printf("warning: could not load shared nav template: %v", err)
+ return
+ }
+ navDefsCache = s
+ })
+ return navDefsCache
}
diff --git a/internal/generator/templates.go b/internal/generator/templates.go
index df854ab..fcb15f5 100644
--- a/internal/generator/templates.go
+++ b/internal/generator/templates.go
@@ -5,6 +5,6 @@ package generator
// binary via embed.FS; see internal/generator/templates/embed.go.
//
// themes.go exposes ListThemes()/validThemeName().
-// shared.go loads templates/shared/nav.tmpl into navDefs (splashGate, navhints,
+// shared.go loads templates/shared/nav.tmpl into getNavDefs() (splashGate, navhints,
// navmodal partials called from shell.tmpl).
// favicon.go generates the favicon.ico binary written into each output dir.
diff --git a/internal/generator/themes.go b/internal/generator/themes.go
index a3a9881..c0ba7ea 100644
--- a/internal/generator/themes.go
+++ b/internal/generator/themes.go
@@ -2,6 +2,7 @@ package generator
import (
"log"
+ "sync"
"codeberg.org/snonux/snonux/internal/generator/templates"
)
@@ -12,30 +13,37 @@ const fallbackThemeName = "neon"
// themeSet caches the list of theme names available in the embedded template FS
// so ListThemes does not re-read the directory on every call.
-var themeSet = loadThemeSet()
-
-func loadThemeSet() map[string]struct{} {
- names, err := templates.ThemeNames()
- if err != nil {
- // At build time the embed //go:embed directive guarantees the FS is
- // populated, so this should never happen; log and continue with an
- // empty set so callers can fall back cleanly.
- log.Printf("warning: could not enumerate themes from embedded FS: %v", err)
- return map[string]struct{}{}
- }
+var (
+ themeSetCache map[string]struct{}
+ themeSetOnce sync.Once
+)
- out := make(map[string]struct{}, len(names))
- for _, n := range names {
- out[n] = struct{}{}
- }
- return out
+func getThemeSet() map[string]struct{} {
+ themeSetOnce.Do(func() {
+ names, err := templates.ThemeNames()
+ if err != nil {
+ // At build time the embed //go:embed directive guarantees the FS is
+ // populated, so this should never happen; log and continue with an
+ // empty set so callers can fall back cleanly.
+ log.Printf("warning: could not enumerate themes from embedded FS: %v", err)
+ themeSetCache = map[string]struct{}{}
+ return
+ }
+
+ out := make(map[string]struct{}, len(names))
+ for _, n := range names {
+ out[n] = struct{}{}
+ }
+ themeSetCache = out
+ })
+ return themeSetCache
}
// validThemeName returns name if it is a known theme, otherwise the fallback.
// Callers use this to coerce CLI input ("--theme random" already resolves
// upstream) so downstream lookups never miss.
func validThemeName(name string) string {
- if _, ok := themeSet[name]; ok {
+ if _, ok := getThemeSet()[name]; ok {
return name
}
return fallbackThemeName