forgejo/modules/theme/theme.go
Minecon724 7556843ec1
Some checks are pending
/ release (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
Add theme info
Display is rough right now but better than nothing. Icons especially to be improved.
2025-05-10 16:45:25 +00:00

189 lines
3.6 KiB
Go

package theme
import (
"bufio"
"errors"
"fmt"
"io"
"maps"
"slices"
"strings"
"forgejo.org/modules/assetfs"
"forgejo.org/modules/log"
"forgejo.org/modules/public"
"forgejo.org/modules/setting"
"golang.org/x/sync/singleflight"
)
var (
group singleflight.Group
assetFs *assetfs.LayeredFS
loaded bool
themes map[string]Theme
)
type Theme struct {
ID string
Name string
Family string
Author string
Url string
Description string
Scheme string
Verified bool
}
// GetThemes gets installed Themes
//
// Note that you can't just run this during init, because webpack mightn't have completed yet.
// Hence, we're loading on first demand.
func GetThemes() (map[string]Theme, error) {
if loaded {
return themes, nil
}
if assetFs == nil {
assetFs = public.AssetFS()
}
_, err, _ := group.Do("load-themes", func() (any, error) {
_themes, err := loadThemesInner(assetFs)
log.Info("Loaded %d themes", len(_themes))
if err != nil {
group.Forget("load-themes")
} else {
setting.UI.Themes = slices.Collect(maps.Keys(_themes))
themes = _themes
loaded = true
}
return nil, err
})
return themes, err
}
func loadThemesInner(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, "theme-") && strings.HasSuffix(entry, ".css")) {
continue
}
id := entry[6 : len(entry)-4]
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", id, err)
}
themes[id] = theme
log.Debug("Found theme: %s (%s)", theme.Name, theme.ID)
}
if len(themes) == 0 {
return nil, fmt.Errorf("no themes found")
}
return themes, nil
}
// getFriendlyThemeName converts an raw theme name (forgejo-dark) to a friendly name (Forgejo Dark)
//
// Example: forgejo-dark -> Forgejo Dark, catppuccin-maroon-auto -> Catppuccin Maroon Auto
func getFriendlyThemeName(themeName string) string {
themeName = strings.ReplaceAll(themeName, "-", " ")
themeName = strings.ToLower(themeName)
themeName = strings.Title(themeName)
return themeName
}
func loadThemeMeta(assetFs *assetfs.LayeredFS, filename string, theme *Theme) error {
file, err := assetFs.Open("assets/css/" + filename)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
kv, err := scanTheme(file, filename[6:len(filename)-4])
if err != nil {
return fmt.Errorf("error scanning theme for meta: %w", err)
}
theme.Name = kv["name"]
theme.Family = kv["family"]
theme.Author = kv["author"]
theme.Url = kv["url"]
theme.Description = kv["description"]
theme.Scheme = strings.ToLower(kv["scheme"])
theme.Verified = kv["verified"] == "yes"
return nil
}
func scanTheme(file io.Reader, id string) (map[string]string, error) {
var kv = make(map[string]string)
scanner := bufio.NewScanner(file)
ok := false
// webpack adds headers
for scanner.Scan() {
if strings.TrimSpace(scanner.Text()) == "/* theme "+id {
ok = true
break
}
}
if !ok {
return nil, errors.New("theme has no meta")
}
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) == 0 {
continue
}
key := strings.ToLower(parts[0])
if key == "*/" {
break
}
if len(parts) == 1 {
kv[key] = "yes"
} else {
kv[key] = strings.Join(parts[1:], " ")
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scanner error: %w", err)
}
return kv, nil
}