forgejo/models/theme/theme_loader.go
2025-06-03 13:50:33 +02:00

107 lines
2 KiB
Go

package theme
import (
"fmt"
"maps"
"slices"
"strings"
"forgejo.org/modules/assetfs"
"forgejo.org/modules/log"
"forgejo.org/modules/public"
"golang.org/x/sync/singleflight"
)
const (
themeFilenamePrefix = "theme-"
themeFilenameSuffix = ".css"
)
var (
group singleflight.Group
assetFs *assetfs.LayeredFS
loaded bool
loadedThemes map[string]Theme // this static variable feels suspicious
)
func GetThemeIds() ([]string, error) {
themes, err := GetThemes()
if err != nil {
return nil, err
}
return slices.Collect(maps.Keys(themes)), nil
}
// GetThemes gets the installed Themes.
//
// It doesn't install them to settings.UI.Themes.
func GetThemes() (map[string]Theme, error) {
if loaded {
return loadedThemes, nil
}
if assetFs == nil {
assetFs = public.AssetFS()
}
_, err, _ := group.Do("load-themes", func() (any, error) {
_themes, err := loadThemes(assetFs)
log.Info("Loaded %d themes", len(_themes))
if err != nil {
// Retry on error TODO might want to improve that
group.Forget("load-themes")
} else {
loadedThemes = _themes
loaded = true
}
return nil, err
})
return loadedThemes, err
}
// loadThemes scans for and loads themes from an asset filesystem.
//
// Returns: a map of theme ID: theme metadata.
func loadThemes(assetFs *assetfs.LayeredFS) (map[string]Theme, error) {
entries, err := assetFs.ListFiles("assets/css")
if err != nil {
return nil, err
}
themes := make(map[string]Theme)
for _, entry := range entries {
if !(strings.HasPrefix(entry, themeFilenamePrefix) && strings.HasSuffix(entry, themeFilenameSuffix)) {
continue
}
id := entry[len(themeFilenamePrefix) : len(entry) - len(themeFilenameSuffix)]
theme := Theme{
ID: id,
Name: getFriendlyThemeName(id),
}
if err := loadThemeMeta(assetFs, entry, &theme); err != nil {
log.Warn("Failed to load meta of theme %s: %s", entry, err)
}
themes[id] = theme
log.Debug("Found theme: %s (%s)", theme.Name, id)
}
if len(themes) == 0 {
return nil, fmt.Errorf("no themes found")
}
return themes, nil
}