From 1b8aa2fc3553e70e94c57ee9d0e1fa7a321ddfc5 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Sat, 13 Sep 2025 10:16:38 +0000 Subject: [PATCH 1/5] Remove accessibility themes --- .../theme-forgejo-auto-deuteranopia-protanopia.css | 2 -- web_src/css/themes/theme-forgejo-auto-tritanopia.css | 2 -- .../theme-forgejo-dark-deuteranopia-protanopia.css | 9 --------- web_src/css/themes/theme-forgejo-dark-tritanopia.css | 9 --------- .../theme-forgejo-light-deuteranopia-protanopia.css | 11 ----------- web_src/css/themes/theme-forgejo-light-tritanopia.css | 11 ----------- 6 files changed, 44 deletions(-) delete mode 100644 web_src/css/themes/theme-forgejo-auto-deuteranopia-protanopia.css delete mode 100644 web_src/css/themes/theme-forgejo-auto-tritanopia.css delete mode 100644 web_src/css/themes/theme-forgejo-dark-deuteranopia-protanopia.css delete mode 100644 web_src/css/themes/theme-forgejo-dark-tritanopia.css delete mode 100644 web_src/css/themes/theme-forgejo-light-deuteranopia-protanopia.css delete mode 100644 web_src/css/themes/theme-forgejo-light-tritanopia.css diff --git a/web_src/css/themes/theme-forgejo-auto-deuteranopia-protanopia.css b/web_src/css/themes/theme-forgejo-auto-deuteranopia-protanopia.css deleted file mode 100644 index 5f97fa377c..0000000000 --- a/web_src/css/themes/theme-forgejo-auto-deuteranopia-protanopia.css +++ /dev/null @@ -1,2 +0,0 @@ -@import "theme-forgejo-light-deuteranopia-protanopia.css"; -@import "theme-forgejo-dark-deuteranopia-protanopia.css" (prefers-color-scheme: dark); diff --git a/web_src/css/themes/theme-forgejo-auto-tritanopia.css b/web_src/css/themes/theme-forgejo-auto-tritanopia.css deleted file mode 100644 index 256a7038a2..0000000000 --- a/web_src/css/themes/theme-forgejo-auto-tritanopia.css +++ /dev/null @@ -1,2 +0,0 @@ -@import "theme-forgejo-light-tritanopia.css"; -@import "theme-forgejo-dark-tritanopia.css" (prefers-color-scheme: dark); diff --git a/web_src/css/themes/theme-forgejo-dark-deuteranopia-protanopia.css b/web_src/css/themes/theme-forgejo-dark-deuteranopia-protanopia.css deleted file mode 100644 index e0765aba4a..0000000000 --- a/web_src/css/themes/theme-forgejo-dark-deuteranopia-protanopia.css +++ /dev/null @@ -1,9 +0,0 @@ -@import "./theme-forgejo-dark.css"; - -:root { - /* removed rows/words: use red colors from vanilla forgejo-dark */ - --color-diff-added-word-bg: #214d88; - --color-diff-added-row-border: #214d88; - --color-diff-added-row-bg: #18184f; - --color-code-bg: #0d1117; -} diff --git a/web_src/css/themes/theme-forgejo-dark-tritanopia.css b/web_src/css/themes/theme-forgejo-dark-tritanopia.css deleted file mode 100644 index e4fc303481..0000000000 --- a/web_src/css/themes/theme-forgejo-dark-tritanopia.css +++ /dev/null @@ -1,9 +0,0 @@ -@import "./theme-forgejo-dark.css"; - -:root { - /* removed rows/words: use red colors from vanilla forgejo-dark */ - --color-diff-added-word-bg: #214d88; - --color-diff-added-row-border: #214d88; - --color-diff-added-row-bg: #152846; - --color-code-bg: #0d1117; -} diff --git a/web_src/css/themes/theme-forgejo-light-deuteranopia-protanopia.css b/web_src/css/themes/theme-forgejo-light-deuteranopia-protanopia.css deleted file mode 100644 index 8744cbb581..0000000000 --- a/web_src/css/themes/theme-forgejo-light-deuteranopia-protanopia.css +++ /dev/null @@ -1,11 +0,0 @@ -@import "./theme-forgejo-light.css"; - -:root { - --color-diff-removed-word-bg: #c8c850; - --color-diff-removed-row-border: #c8c850; - --color-diff-removed-row-bg: #ffecc4; - --color-diff-added-word-bg: #b8c0ff; - --color-diff-added-row-border: #b8c0ff; - --color-diff-added-row-bg: #e0e0ff; - --color-code-bg: #ffffff; -} diff --git a/web_src/css/themes/theme-forgejo-light-tritanopia.css b/web_src/css/themes/theme-forgejo-light-tritanopia.css deleted file mode 100644 index 3f875e4a7f..0000000000 --- a/web_src/css/themes/theme-forgejo-light-tritanopia.css +++ /dev/null @@ -1,11 +0,0 @@ -@import "./theme-forgejo-light.css"; - -:root { - --color-diff-removed-word-bg: #ffb8c0; - --color-diff-removed-row-border: #ffb8c0; - --color-diff-removed-row-bg: #ffd8d8; - --color-diff-added-word-bg: #b8c0ff; - --color-diff-added-row-border: #b8c0ff; - --color-diff-added-row-bg: #c0e8ff; - --color-code-bg: #ffffff; -} -- 2.49.1 From 933ad0b23a8878ca019e63bd678203612ee58279 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Sat, 13 Sep 2025 10:16:51 +0000 Subject: [PATCH 2/5] Add metadata to builtin themes --- web_src/css/themes/theme-forgejo-auto.css | 10 ++++++++++ web_src/css/themes/theme-forgejo-dark.css | 10 ++++++++++ web_src/css/themes/theme-forgejo-light.css | 10 ++++++++++ web_src/css/themes/theme-gitea-auto.css | 10 ++++++++++ web_src/css/themes/theme-gitea-dark.css | 10 ++++++++++ web_src/css/themes/theme-gitea-light.css | 10 ++++++++++ 6 files changed, 60 insertions(+) diff --git a/web_src/css/themes/theme-forgejo-auto.css b/web_src/css/themes/theme-forgejo-auto.css index ebf59942ea..7f0b932752 100644 --- a/web_src/css/themes/theme-forgejo-auto.css +++ b/web_src/css/themes/theme-forgejo-auto.css @@ -1,2 +1,12 @@ +/* theme forgejo-auto + Name Forgejo + Family Forgejo + Author Forgejo authors + Url https://forgejo.org/ + Description The fresh look of freedom. + Scheme Auto + Recommended +*/ + @import "theme-forgejo-light.css"; @import "theme-forgejo-dark.css" (prefers-color-scheme: dark); diff --git a/web_src/css/themes/theme-forgejo-dark.css b/web_src/css/themes/theme-forgejo-dark.css index 94097497bf..6b55a04b5e 100644 --- a/web_src/css/themes/theme-forgejo-dark.css +++ b/web_src/css/themes/theme-forgejo-dark.css @@ -1,3 +1,13 @@ +/* theme forgejo-dark + Name Forgejo + Family Forgejo + Author Forgejo authors + Url https://forgejo.org/ + Description The fresh look of freedom. + Scheme Dark + Recommended +*/ + @import "../chroma/dark.css"; @import "../codemirror/dark.css"; @import "../markup/dark.css"; diff --git a/web_src/css/themes/theme-forgejo-light.css b/web_src/css/themes/theme-forgejo-light.css index 44b997b39c..dc6356eecb 100644 --- a/web_src/css/themes/theme-forgejo-light.css +++ b/web_src/css/themes/theme-forgejo-light.css @@ -1,3 +1,13 @@ +/* theme forgejo-light + Name Forgejo + Family Forgejo + Author Forgejo authors + Url https://forgejo.org/ + Description The fresh look of freedom. + Scheme Light + Recommended +*/ + @import "../chroma/light.css"; @import "../codemirror/light.css"; @import "../markup/light.css"; diff --git a/web_src/css/themes/theme-gitea-auto.css b/web_src/css/themes/theme-gitea-auto.css index 509889e802..0d0ddc49c7 100644 --- a/web_src/css/themes/theme-gitea-auto.css +++ b/web_src/css/themes/theme-gitea-auto.css @@ -1,2 +1,12 @@ +/* theme gitea-auto + Name Gitea + Family Gitea + Author The Gitea Authors + Url https://github.com/go-gitea/gitea + Description The OG style. + Scheme Auto + Recommended +*/ + @import "./theme-gitea-light.css" (prefers-color-scheme: light); @import "./theme-gitea-dark.css" (prefers-color-scheme: dark); diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index 84183a9e63..8e29b2b29e 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -1,3 +1,13 @@ +/* theme gitea-dark + Name Gitea + Family Gitea + Author The Gitea Authors + Url https://github.com/go-gitea/gitea + Description The OG style. + Scheme Dark + Recommended +*/ + @import "../chroma/dark.css"; @import "../codemirror/dark.css"; @import "../markup/dark.css"; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index aee47dc814..7182619992 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -1,3 +1,13 @@ +/* theme gitea-light + Name Gitea + Family Gitea + Author The Gitea Authors + Url https://github.com/go-gitea/gitea + Description The OG style. + Scheme Light + Recommended +*/ + @import "../chroma/light.css"; @import "../codemirror/light.css"; @import "../markup/light.css"; -- 2.49.1 From adbd5af8d125a4c67f9538738ab2f124b388593a Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Sat, 13 Sep 2025 10:17:00 +0000 Subject: [PATCH 3/5] Fix webpack --- tools/watch.sh | 9 +++++++-- webpack.config.js | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/watch.sh b/tools/watch.sh index 5e8defa49c..8c63d44b59 100644 --- a/tools/watch.sh +++ b/tools/watch.sh @@ -1,8 +1,13 @@ #!/bin/bash set -euo pipefail -make --no-print-directory watch-frontend & -make --no-print-directory watch-backend & +make --no-print-directory watch-frontend | tee >( + awk -v pattern="webpack" ' + $0 ~ pattern { + system("make --no-print-directory watch-backend &") + } + ' +) & trap 'kill $(jobs -p)' EXIT wait diff --git a/webpack.config.js b/webpack.config.js index 8f9949d7b1..90f8daea32 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -157,6 +157,7 @@ export default { minify: true, css: true, legalComments: 'none', + exclude: [new RegExp(`css\\/(${Object.keys(themes).join('|')})\\.css$`)], }), ], splitChunks: { -- 2.49.1 From fa533b8ba5b25bd8073f9f699cdba470d028c93e Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Sat, 13 Sep 2025 10:46:40 +0000 Subject: [PATCH 4/5] Add theme loader --- cmd/web.go | 8 +++ models/theme/theme_loader.go | 107 ++++++++++++++++++++++++++++++++++ models/theme/theme_meta.go | 108 +++++++++++++++++++++++++++++++++++ models/theme/theme_name.go | 14 +++++ 4 files changed, 237 insertions(+) create mode 100644 models/theme/theme_loader.go create mode 100644 models/theme/theme_meta.go create mode 100644 models/theme/theme_name.go diff --git a/cmd/web.go b/cmd/web.go index 87965a7c1e..f455b97180 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -16,6 +16,7 @@ import ( _ "net/http/pprof" // Used for debugging if enabled and a web server is running + "forgejo.org/models/theme" "forgejo.org/modules/container" "forgejo.org/modules/graceful" "forgejo.org/modules/log" @@ -217,6 +218,13 @@ func serveInstalled(_ context.Context, ctx *cli.Command) error { } } + // Load themes + themes, tlerr := theme.GetThemeIds() + if (tlerr != nil) { + log.Error("Failed to load themes: ", tlerr) + } + setting.UI.Themes = themes + // Set up Chi routes webRoutes := routers.NormalRoutes() err := listen(webRoutes, true) diff --git a/models/theme/theme_loader.go b/models/theme/theme_loader.go new file mode 100644 index 0000000000..e3488b093b --- /dev/null +++ b/models/theme/theme_loader.go @@ -0,0 +1,107 @@ +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 +} diff --git a/models/theme/theme_meta.go b/models/theme/theme_meta.go new file mode 100644 index 0000000000..5e14da3a5c --- /dev/null +++ b/models/theme/theme_meta.go @@ -0,0 +1,108 @@ +package theme + +import ( + "bufio" + "errors" + "fmt" + "io" + "strings" + + "forgejo.org/modules/assetfs" +) + +// The Theme struct represents the metadata of a theme. Most fields are allowed to be null / blank. +type Theme struct { + // ID is the theme id, e.g., forgejo-dark + ID string + // Name is the theme's readable name, e.g., Forgejo. Note that the scheme is excluded in favor of the Scheme property. + Name string + // Family is the theme's family, e.g. Catppuccin. + Family string + // Author is the theme's author. It may be an invididual, a group, or an entity. + Author string + // Url is the URL to the theme's website, e.g. https://forgejo.org. + Url string + // Description is the theme's short description. + Description string + // Scheme is the theme's color scheme. It must be either: dark, light, or auto. + Scheme string + // Recommended means the theme is guaranteed to be bug-free, usually a first-party theme. + Recommended bool +} + +// loadThemeMeta loads theme metadata from a file in a layered FS. +// +// Theme ID is expected to be prefilled. The metadata will be assigned to the passed `theme` object. +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 := scanThemeForKeyValueMetaMap(file, theme.ID) // ID is expected to be prefilled + + 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.Recommended = kv["recommended"] == "yes" + + return nil +} + +// scanThemeForKeyValueMetaMap finds raw key-value metadata in an open file. +func scanThemeForKeyValueMetaMap(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 +} diff --git a/models/theme/theme_name.go b/models/theme/theme_name.go new file mode 100644 index 0000000000..eeaa4bf568 --- /dev/null +++ b/models/theme/theme_name.go @@ -0,0 +1,14 @@ +package theme + +import "strings" + +// getFriendlyThemeName converts an raw theme name to a friendly, readable name. +// 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 +} -- 2.49.1 From c92f17c167a1093412deac587cba70d140043bac Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Sat, 13 Sep 2025 10:46:57 +0000 Subject: [PATCH 5/5] Display everything in the UI --- options/locale/locale_en-US.ini | 4 ++ routers/web/user/setting/profile.go | 15 +++-- templates/user/settings/appearance.tmpl | 85 ++++++++++++++++--------- 3 files changed, 68 insertions(+), 36 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 0614bcf167..d8e3f90777 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -812,6 +812,10 @@ manage_themes = Default theme manage_openid = OpenID addresses email_desc = Your primary email address will be used for notifications, password recovery and, provided that it is not hidden, web-based Git operations. theme_desc = This theme will be used for the web interface when you are logged in. +theme_recommended = Recommended +theme_light = Light +theme_dark = Dark +theme_auto = Auto (per system) primary = Primary activated = Activated requires_activation = Requires activation diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index e0ce88b582..d76dfc111a 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -19,6 +19,7 @@ import ( "forgejo.org/models/db" "forgejo.org/models/organization" repo_model "forgejo.org/models/repo" + "forgejo.org/models/theme" user_model "forgejo.org/models/user" "forgejo.org/modules/base" "forgejo.org/modules/log" @@ -335,15 +336,15 @@ func Repos(ctx *context.Context) { func Appearance(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.appearance") ctx.Data["PageIsSettingsAppearance"] = true - ctx.Data["AllThemes"] = setting.UI.Themes - ctx.Data["ThemeName"] = func(themeName string) string { - fullThemeName := "themes.names." + themeName - if ctx.Locale.HasKey(fullThemeName) { - return ctx.Locale.TrString(fullThemeName) - } - return themeName + + themes, err := theme.GetThemes() + if err != nil { + ctx.ServerError("Failed to load themes", err) + return } + ctx.Data["AllThemes"] = themes + var hiddenCommentTypes *big.Int val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes) if err != nil { diff --git a/templates/user/settings/appearance.tmpl b/templates/user/settings/appearance.tmpl index df4d6f3999..a0db0eb9c6 100644 --- a/templates/user/settings/appearance.tmpl +++ b/templates/user/settings/appearance.tmpl @@ -7,37 +7,64 @@
-- 2.49.1