summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-16 09:35:31 +0300
committerPaul Buetow <paul@buetow.org>2026-05-16 09:35:31 +0300
commit1b24076788868f50812468b1bf4bb0ae86214e8c (patch)
tree389fd715b5c27e0fe036f6c123b8d09e3872da1c
parentfbac3d13a575a410c2ee3f49418bf4b462544b4f (diff)
Add theme background musicv0.16.0
-rw-r--r--cmd/snonux/sync_test.go47
-rw-r--r--integrationtests/integration_test.go38
-rw-r--r--internal/generator/generator.go31
-rw-r--r--internal/generator/generator_test.go24
-rw-r--r--internal/generator/templates/embed.go3
-rw-r--r--internal/generator/templates/shared/nav.tmpl61
-rw-r--r--internal/generator/templates/shared/shared.css53
-rw-r--r--internal/generator/templates/shared/shared.js224
-rw-r--r--internal/generator/templates/themes/aurora/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/aurora/ambient.oggbin0 -> 1396111 bytes
-rw-r--r--internal/generator/templates/themes/aurora/sounds.json8
-rw-r--r--internal/generator/templates/themes/aurora/theme.css2
-rw-r--r--internal/generator/templates/themes/biomech/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/biomech/ambient.oggbin0 -> 1317802 bytes
-rw-r--r--internal/generator/templates/themes/biomech/sounds.json8
-rw-r--r--internal/generator/templates/themes/biomech/theme.css2
-rw-r--r--internal/generator/templates/themes/brutalist/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/brutalist/ambient.oggbin0 -> 1428440 bytes
-rw-r--r--internal/generator/templates/themes/brutalist/sounds.json8
-rw-r--r--internal/generator/templates/themes/brutalist/theme.css2
-rw-r--r--internal/generator/templates/themes/cathedral/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/cathedral/ambient.oggbin0 -> 1048852 bytes
-rw-r--r--internal/generator/templates/themes/cathedral/sounds.json8
-rw-r--r--internal/generator/templates/themes/cathedral/theme.css2
-rw-r--r--internal/generator/templates/themes/cosmos/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/cosmos/ambient.oggbin0 -> 2706024 bytes
-rw-r--r--internal/generator/templates/themes/cosmos/sounds.json8
-rw-r--r--internal/generator/templates/themes/cosmos/theme.css2
-rw-r--r--internal/generator/templates/themes/dos/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/dos/ambient.oggbin0 -> 1039452 bytes
-rw-r--r--internal/generator/templates/themes/dos/sounds.json8
-rw-r--r--internal/generator/templates/themes/dos/theme.css2
-rw-r--r--internal/generator/templates/themes/matrix/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/matrix/ambient.oggbin0 -> 276371 bytes
-rw-r--r--internal/generator/templates/themes/matrix/sounds.json8
-rw-r--r--internal/generator/templates/themes/matrix/theme.css2
-rw-r--r--internal/generator/templates/themes/neon/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/neon/ambient.oggbin0 -> 3565740 bytes
-rw-r--r--internal/generator/templates/themes/neon/sounds.json8
-rw-r--r--internal/generator/templates/themes/noir/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/noir/ambient.oggbin0 -> 2440360 bytes
-rw-r--r--internal/generator/templates/themes/noir/sounds.json8
-rw-r--r--internal/generator/templates/themes/noir/theme.css2
-rw-r--r--internal/generator/templates/themes/nukem/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/nukem/ambient.oggbin0 -> 691008 bytes
-rw-r--r--internal/generator/templates/themes/nukem/sounds.json8
-rw-r--r--internal/generator/templates/themes/nukem/theme.css2
-rw-r--r--internal/generator/templates/themes/ocean/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/ocean/ambient.oggbin0 -> 2313143 bytes
-rw-r--r--internal/generator/templates/themes/ocean/sounds.json8
-rw-r--r--internal/generator/templates/themes/ocean/theme.css2
-rw-r--r--internal/generator/templates/themes/plasma/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/plasma/ambient.oggbin0 -> 1949772 bytes
-rw-r--r--internal/generator/templates/themes/plasma/sounds.json8
-rw-r--r--internal/generator/templates/themes/plasma/theme.css2
-rw-r--r--internal/generator/templates/themes/retro/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/retro/ambient.oggbin0 -> 1937080 bytes
-rw-r--r--internal/generator/templates/themes/retro/sounds.json8
-rw-r--r--internal/generator/templates/themes/retro/theme.css2
-rw-r--r--internal/generator/templates/themes/retrofuture/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/retrofuture/ambient.oggbin0 -> 500879 bytes
-rw-r--r--internal/generator/templates/themes/retrofuture/sounds.json8
-rw-r--r--internal/generator/templates/themes/retrofuture/theme.css2
-rw-r--r--internal/generator/templates/themes/spaceage/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/spaceage/ambient.oggbin0 -> 477361 bytes
-rw-r--r--internal/generator/templates/themes/spaceage/sounds.json8
-rw-r--r--internal/generator/templates/themes/spaceage/theme.css2
-rw-r--r--internal/generator/templates/themes/surveillance/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/surveillance/ambient.oggbin0 -> 148158 bytes
-rw-r--r--internal/generator/templates/themes/surveillance/sounds.json8
-rw-r--r--internal/generator/templates/themes/surveillance/theme.css2
-rw-r--r--internal/generator/templates/themes/synthwave/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/synthwave/ambient.oggbin0 -> 655778 bytes
-rw-r--r--internal/generator/templates/themes/synthwave/sounds.json8
-rw-r--r--internal/generator/templates/themes/synthwave/theme.css2
-rw-r--r--internal/generator/templates/themes/terminal/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/terminal/ambient.oggbin0 -> 888400 bytes
-rw-r--r--internal/generator/templates/themes/terminal/sounds.json8
-rw-r--r--internal/generator/templates/themes/terminal/theme.css2
-rw-r--r--internal/generator/templates/themes/tropicale/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/tropicale/ambient.oggbin0 -> 363392 bytes
-rw-r--r--internal/generator/templates/themes/tropicale/sounds.json8
-rw-r--r--internal/generator/templates/themes/tropicale/theme.css2
-rw-r--r--internal/generator/templates/themes/volcano/MUSIC_LICENSE.txt7
-rw-r--r--internal/generator/templates/themes/volcano/ambient.oggbin0 -> 977074 bytes
-rw-r--r--internal/generator/templates/themes/volcano/sounds.json8
-rw-r--r--internal/generator/templates/themes/volcano/theme.css2
-rw-r--r--internal/generator/theme_sounds.go2
-rw-r--r--internal/version/version.go2
89 files changed, 712 insertions, 111 deletions
diff --git a/cmd/snonux/sync_test.go b/cmd/snonux/sync_test.go
index 93c9233..b8e8701 100644
--- a/cmd/snonux/sync_test.go
+++ b/cmd/snonux/sync_test.go
@@ -31,7 +31,8 @@ func TestSplitAndTrim(t *testing.T) {
}
func TestResolveSyncConfig_defaults(t *testing.T) {
- t.Parallel()
+ unsetEnv(t, "SNONUX_SYNC_TARGETS")
+ unsetEnv(t, "SNONUX_SYNC_REMOTE_DIR")
cfg := &config.Config{}
resolveSyncConfig(cfg)
@@ -45,12 +46,8 @@ func TestResolveSyncConfig_defaults(t *testing.T) {
}
func TestResolveSyncConfig_envTargets(t *testing.T) {
- t.Parallel()
-
- orig := os.Getenv("SNONUX_SYNC_TARGETS")
- defer os.Setenv("SNONUX_SYNC_TARGETS", orig)
-
- os.Setenv("SNONUX_SYNC_TARGETS", "h1, h2 ,h3")
+ t.Setenv("SNONUX_SYNC_TARGETS", "h1, h2 ,h3")
+ unsetEnv(t, "SNONUX_SYNC_REMOTE_DIR")
cfg := &config.Config{}
resolveSyncConfig(cfg)
want := []string{"h1", "h2", "h3"}
@@ -60,12 +57,8 @@ func TestResolveSyncConfig_envTargets(t *testing.T) {
}
func TestResolveSyncConfig_envRemoteDir(t *testing.T) {
- t.Parallel()
-
- orig := os.Getenv("SNONUX_SYNC_REMOTE_DIR")
- defer os.Setenv("SNONUX_SYNC_REMOTE_DIR", orig)
-
- os.Setenv("SNONUX_SYNC_REMOTE_DIR", "/custom/path/")
+ unsetEnv(t, "SNONUX_SYNC_TARGETS")
+ t.Setenv("SNONUX_SYNC_REMOTE_DIR", "/custom/path/")
cfg := &config.Config{}
resolveSyncConfig(cfg)
if cfg.SyncRemoteDir != "/custom/path/" {
@@ -74,17 +67,8 @@ func TestResolveSyncConfig_envRemoteDir(t *testing.T) {
}
func TestResolveSyncConfig_flagsOverrideEnv(t *testing.T) {
- t.Parallel()
-
- origTargets := os.Getenv("SNONUX_SYNC_TARGETS")
- origDir := os.Getenv("SNONUX_SYNC_REMOTE_DIR")
- defer func() {
- os.Setenv("SNONUX_SYNC_TARGETS", origTargets)
- os.Setenv("SNONUX_SYNC_REMOTE_DIR", origDir)
- }()
-
- os.Setenv("SNONUX_SYNC_TARGETS", "from-env")
- os.Setenv("SNONUX_SYNC_REMOTE_DIR", "/env/dir/")
+ t.Setenv("SNONUX_SYNC_TARGETS", "from-env")
+ t.Setenv("SNONUX_SYNC_REMOTE_DIR", "/env/dir/")
cfg := &config.Config{
SyncTargets: []string{"from-flag"},
@@ -99,3 +83,18 @@ func TestResolveSyncConfig_flagsOverrideEnv(t *testing.T) {
t.Fatalf("flag dir should override env: got %q", cfg.SyncRemoteDir)
}
}
+
+func unsetEnv(t *testing.T, key string) {
+ t.Helper()
+ orig, hadOrig := os.LookupEnv(key)
+ if err := os.Unsetenv(key); err != nil {
+ t.Fatalf("unset %s: %v", key, err)
+ }
+ t.Cleanup(func() {
+ if hadOrig {
+ _ = os.Setenv(key, orig)
+ } else {
+ _ = os.Unsetenv(key)
+ }
+ })
+}
diff --git a/integrationtests/integration_test.go b/integrationtests/integration_test.go
index 762bcb8..6a4d437 100644
--- a/integrationtests/integration_test.go
+++ b/integrationtests/integration_test.go
@@ -24,6 +24,7 @@ import (
"codeberg.org/snonux/snonux/internal/config"
"codeberg.org/snonux/snonux/internal/generator"
"codeberg.org/snonux/snonux/internal/processor"
+ "codeberg.org/snonux/snonux/internal/version"
)
var ctx = context.Background() //nolint:gochecknoglobals // test-only top-level helper used by every test in the file
@@ -391,8 +392,8 @@ func TestKeyboardNavJS(t *testing.T) {
assertContains(t, sharedJS, "triggerFlashEffect();", "shared.js f triggers flash")
// Nav hints and splash hints should display the updated keys.
- assertContains(t, index, "<kbd>p</kbd> music", "index.html nav hint p=ambient")
- assertContains(t, index, "<kbd>f</kbd> flash", "index.html nav hint f=flash")
+ assertContains(t, index, "<kbd>p</kbd><span class=\"sno-btn-text\">music</span>", "index.html nav hint p=ambient")
+ assertContains(t, index, "<kbd>f</kbd><span class=\"sno-btn-text\">flash</span>", "index.html nav hint f=flash")
}
// TestScrollDrivenPostSelection verifies the generated page behavior in a real
@@ -622,10 +623,12 @@ func TestThemeSelection(t *testing.T) {
assertContains(t, index, "theme test post", "post text")
assertContains(t, index, `data-index="0"`, "data-index attribute")
assertContains(t, index, `SNONUX_DEFAULT_THEME = "`+theme+`"`, "default theme baked in")
+ assertContains(t, index, "snonux v"+version.Version, "index.html header version")
// Per-theme assets are written under themes/<theme>/.
+ themeDir := filepath.Join(outputDir, "themes", theme)
for _, fname := range []string{"theme.css", "theme.js", "meta.json", "sounds.json"} {
- path := filepath.Join(outputDir, "themes", theme, fname)
+ path := filepath.Join(themeDir, fname)
info, err := os.Stat(path)
if err != nil {
t.Fatalf("theme asset missing %s: %v", path, err)
@@ -665,7 +668,6 @@ func TestThemeSelection(t *testing.T) {
},
}
if assets, ok := fontAssets[theme]; ok {
- themeDir := filepath.Join(outputDir, "themes", theme)
for _, fname := range assets {
path := filepath.Join(themeDir, fname)
info, err := os.Stat(path)
@@ -707,6 +709,34 @@ func TestThemeSelection(t *testing.T) {
t.Errorf("sounds.json ambient missing %q variant for theme %q", key, theme)
}
}
+ for _, key := range []string{"normal", "wild"} {
+ preset, ok := ambient[key].(map[string]interface{})
+ if !ok {
+ continue
+ }
+ if preset["file"] != "ambient.ogg" {
+ t.Errorf("sounds.json ambient.%s file=%v for theme %q; want ambient.ogg", key, preset["file"], theme)
+ }
+ }
+ for _, asset := range []string{"ambient.ogg", "MUSIC_LICENSE.txt"} {
+ if _, err := os.Stat(filepath.Join(themeDir, asset)); err != nil {
+ t.Errorf("%s missing generated %s: %v", theme, asset, err)
+ }
+ }
+
+ metaData, err := os.ReadFile(filepath.Join(themeDir, "meta.json"))
+ if err != nil {
+ t.Fatalf("read generated meta.json: %v", err)
+ }
+ var meta struct {
+ HeaderHTML string `json:"header_html"`
+ }
+ if err := json.Unmarshal(metaData, &meta); err != nil {
+ t.Fatalf("generated meta.json invalid JSON: %v", err)
+ }
+ if !strings.Contains(meta.HeaderHTML, "snonux v"+version.Version) {
+ t.Errorf("%s generated meta.json header missing version", theme)
+ }
// shared.js holds the nav logic.
sharedJS := readFile(t, filepath.Join(outputDir, "shared.js"))
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index aae4c97..feefe18 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -15,6 +15,7 @@ import (
"codeberg.org/snonux/snonux/internal/generator/atom"
"codeberg.org/snonux/snonux/internal/generator/templates"
"codeberg.org/snonux/snonux/internal/post"
+ "codeberg.org/snonux/snonux/internal/version"
)
// pageData holds the template variables for a single HTML page.
@@ -339,7 +340,7 @@ func writeThemeAsset(dir, name string) error {
return fmt.Errorf("write %s/theme.js: %w", name, err)
}
- meta, err := templates.ThemeMeta(name)
+ meta, err := themeMetaJSON(name)
if err != nil {
return fmt.Errorf("read %s/meta.json: %w", name, err)
}
@@ -376,9 +377,37 @@ func loadThemeMeta(name string) (themeMeta, error) {
if err := json.Unmarshal(b, &m); err != nil {
return m, fmt.Errorf("parse theme meta %q: %w", name, err)
}
+ m.HeaderHTML = headerHTMLWithVersion(m.HeaderHTML)
return m, nil
}
+func themeMetaJSON(name string) ([]byte, error) {
+ m, err := loadThemeMeta(name)
+ if err != nil {
+ return nil, err
+ }
+ b, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return nil, fmt.Errorf("marshal theme meta %q: %w", name, err)
+ }
+ return b, nil
+}
+
+func headerHTMLWithVersion(header string) string {
+ if strings.Contains(header, "sno-version") {
+ return header
+ }
+ versionHTML := ` <span class="sno-version-sep" aria-hidden="true">&middot;</span> <span class="sno-version">snonux v` + template.HTMLEscapeString(version.Version) + `</span>`
+ hostClass := `class="logo-host"`
+ if idx := strings.Index(header, hostClass); idx >= 0 {
+ if end := strings.Index(header[idx:], "</p>"); end >= 0 {
+ pos := idx + end
+ return header[:pos] + versionHTML + header[pos:]
+ }
+ }
+ return header + versionHTML
+}
+
// allThemesJSON returns a JS array literal of all theme names.
func allThemesJSON() (template.JS, error) {
names, err := templates.ThemeNames()
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 7e2742b..a718c95 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -12,6 +12,7 @@ import (
"codeberg.org/snonux/snonux/internal/config"
"codeberg.org/snonux/snonux/internal/post"
+ "codeberg.org/snonux/snonux/internal/version"
)
var ctx = context.Background() //nolint:gochecknoglobals // test-only top-level helper used by every test in the file
@@ -94,6 +95,23 @@ func TestJSONStringOrNull(t *testing.T) {
}
}
+func TestLoadThemeMetaInjectsVersion(t *testing.T) {
+ t.Parallel()
+
+ for name := range getThemeSet() {
+ meta, err := loadThemeMeta(name)
+ if err != nil {
+ t.Fatalf("loadThemeMeta(%q): %v", name, err)
+ }
+ if !strings.Contains(meta.HeaderHTML, "snonux v"+version.Version) {
+ t.Errorf("theme %q header missing snonux version", name)
+ }
+ if strings.Count(meta.HeaderHTML, `class="sno-version"`) != 1 {
+ t.Errorf("theme %q header has duplicate version markup", name)
+ }
+ }
+}
+
func TestThemeSoundsJSONNonEmpty(t *testing.T) {
t.Parallel()
j := themeSoundsJSON("neon")
@@ -181,6 +199,12 @@ func TestThemeSoundPresetsAmbientValuesBounded(t *testing.T) {
if a.Gain <= 0 || a.Gain > 0.15 {
t.Errorf("theme %q ambient.%s gain=%f; want (0, 0.15]", name, mode, a.Gain)
}
+ if a.File != "ambient.ogg" {
+ t.Errorf("theme %q ambient.%s file=%q; want ambient.ogg", name, mode, a.File)
+ }
+ if a.Volume <= 0 || a.Volume > 0.8 {
+ t.Errorf("theme %q ambient.%s volume=%f; want (0, 0.8]", name, mode, a.Volume)
+ }
if a.BPM <= 0 || a.BPM > 400 {
t.Errorf("theme %q ambient.%s bpm=%f; want (0, 250]", name, mode, a.BPM)
}
diff --git a/internal/generator/templates/embed.go b/internal/generator/templates/embed.go
index 56fc673..191f4ad 100644
--- a/internal/generator/templates/embed.go
+++ b/internal/generator/templates/embed.go
@@ -20,6 +20,7 @@ import (
// theme.js — required, always present
// meta.json — required, always present
// sounds.json — required, always present
+// ambient.ogg — optional, background music loop
// <Family-Weight>.woff2 — optional, one or more self-hosted web fonts
// <Family-Weight>.woff — optional, fallback / non-woff2 web fonts
// FONT_LICENSE.txt — required iff any font file is present;
@@ -34,7 +35,7 @@ import (
// match at least one file. When you introduce a new font extension
// that no other theme uses yet, append the matching glob here.
//
-//go:embed shell.tmpl shared/*.tmpl shared/shared.css shared/shared.js themes/*/theme.css themes/*/theme.js themes/*/meta.json themes/*/sounds.json themes/*/*.woff themes/*/*.woff2 themes/*/FONT_LICENSE.txt
+//go:embed shell.tmpl shared/*.tmpl shared/shared.css shared/shared.js themes/*/theme.css themes/*/theme.js themes/*/meta.json themes/*/sounds.json themes/*/*.ogg themes/*/*.woff themes/*/*.woff2 themes/*/FONT_LICENSE.txt themes/*/MUSIC_LICENSE.txt
var FS embed.FS
// themeStandardFiles lists the per-theme files that have dedicated
diff --git a/internal/generator/templates/shared/nav.tmpl b/internal/generator/templates/shared/nav.tmpl
index b04b160..9759761 100644
--- a/internal/generator/templates/shared/nav.tmpl
+++ b/internal/generator/templates/shared/nav.tmpl
@@ -44,38 +44,65 @@
{{end}}
{{define "navhints"}}
-<div class="nav-hints" role="region" aria-label="Keyboard shortcuts">
+<div class="nav-hints" role="region" aria-label="Controls and shortcuts">
<div class="nav-hints-row nav-hints-nav">
<span class="nav-hints-label">nav</span>
- <span><kbd>j</kbd><kbd>k</kbd><kbd>↑</kbd><kbd>↓</kbd> post</span>
- <span><kbd>h</kbd><kbd>l</kbd><kbd>←</kbd><kbd>→</kbd> page</span>
- <span><kbd>PgUp</kbd><kbd>PgDn</kbd> scroll</span>
- <span><kbd>Enter</kbd> open</span>
- <span><kbd>Esc</kbd> close</span>
+ <button type="button" class="nav-fx-button sno-nav-btn" data-sno-nav="prev-post" aria-label="Previous post">
+ <i class="fas fa-arrow-up" aria-hidden="true"></i><kbd>j</kbd><kbd>↑</kbd><span class="sno-btn-text">post</span>
+ </button>
+ <button type="button" class="nav-fx-button sno-nav-btn" data-sno-nav="next-post" aria-label="Next post">
+ <i class="fas fa-arrow-down" aria-hidden="true"></i><kbd>k</kbd><kbd>↓</kbd><span class="sno-btn-text">post</span>
+ </button>
+