detached 24d05c8

Merge pull request #6 from bnema/feature/color-scheme-config

feat: add color_scheme config for theme preference
commit
24d05c83ba3decd3b8d04b9cb5a4da95f97d60cc
author
bnema · · 2025-10-23 21:14 UTC
committer
GitHub · · 2025-10-23 21:14 UTC
merge of
1453d21 + 86ddc8f · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
6 files · +74 -4
M CHANGELOG.md +3 -0
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44
55 ## [Unreleased]
66
7+### Added
8+- **Color scheme config**: `appearance.color_scheme` setting to force dark/light theme (`"prefer-dark"`, `"prefer-light"`, `"default"`)
9+
710 ## [0.12.1] - 2025-10-18
811
912 ### Added
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
4 4
5 ## [Unreleased]5 ## [Unreleased]
6 6
7+### Added
8+- **Color scheme config**: `appearance.color_scheme` setting to force dark/light theme (`"prefer-dark"`, `"prefer-light"`, `"default"`)
9+
7 ## [0.12.1] - 2025-10-1810 ## [0.12.1] - 2025-10-18
8 11
9 ### Added12 ### Added
M README.md +2 -0
@@ -275,6 +275,7 @@ A default `config.json` is created on first run. Config changes are watched and
275275 #### Appearance Settings
276276 ```json
277277 "appearance": {
278+ "color_scheme": "default",
278279 "sans_font": "Fira Sans",
279280 "serif_font": "Fira Sans",
280281 "monospace_font": "Fira Code",
@@ -300,6 +301,7 @@ A default `config.json` is created on first run. Config changes are watched and
300301 }
301302 }
302303 ```
304+- `color_scheme`: `"prefer-dark"`, `"prefer-light"`, or `"default"` (follows system theme)
303305
304306 #### Rendering Mode (GPU/CPU)
305307 ```json
@@ -275,6 +275,7 @@ A default `config.json` is created on first run. Config changes are watched and
275 #### Appearance Settings275 #### Appearance Settings
276 ```json276 ```json
277 "appearance": {277 "appearance": {
278+ "color_scheme": "default",
278 "sans_font": "Fira Sans",279 "sans_font": "Fira Sans",
279 "serif_font": "Fira Sans",280 "serif_font": "Fira Sans",
280 "monospace_font": "Fira Code",281 "monospace_font": "Fira Code",
@@ -300,6 +301,7 @@ A default `config.json` is created on first run. Config changes are watched and
300 }301 }
301 }302 }
302 ```303 ```
304+- `color_scheme`: `"prefer-dark"`, `"prefer-light"`, or `"default"` (follows system theme)
303 305
304 #### Rendering Mode (GPU/CPU)306 #### Rendering Mode (GPU/CPU)
305 ```json307 ```json
M internal/config/config.go +27 -0
@@ -124,6 +124,8 @@ type AppearanceConfig struct {
124124 DefaultFontSize int `mapstructure:"default_font_size" yaml:"default_font_size"`
125125 LightPalette ColorPalette `mapstructure:"light_palette" yaml:"light_palette"`
126126 DarkPalette ColorPalette `mapstructure:"dark_palette" yaml:"dark_palette"`
127+ // ColorScheme controls the initial theme preference: "prefer-dark", "prefer-light", or "default" (follows system)
128+ ColorScheme string `mapstructure:"color_scheme" yaml:"color_scheme"`
127129 }
128130
129131 // ColorPalette contains semantic color tokens for light/dark themes.
@@ -545,6 +547,9 @@ func (m *Manager) Load() error {
545547 // Validate and configure codec preferences
546548 config = m.validateAndConfigureCodecPreferences(config)
547549
550+ // Validate ColorScheme setting
551+ m.validateColorScheme(config)
552+
548553 m.config = config
549554 return nil
550555 }
@@ -643,6 +648,9 @@ func (m *Manager) reload() error {
643648 // Validate and configure codec preferences
644649 config = m.validateAndConfigureCodecPreferences(config)
645650
651+ // Validate ColorScheme setting
652+ m.validateColorScheme(config)
653+
646654 m.config = config
647655 return nil
648656 }
@@ -713,6 +721,7 @@ func (m *Manager) setDefaults() {
713721 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
714722 m.viper.SetDefault("appearance.light_palette", defaults.Appearance.LightPalette)
715723 m.viper.SetDefault("appearance.dark_palette", defaults.Appearance.DarkPalette)
724+ m.viper.SetDefault("appearance.color_scheme", defaults.Appearance.ColorScheme)
716725
717726 // Video acceleration defaults
718727 m.viper.SetDefault("video_acceleration.enable_vaapi", defaults.VideoAcceleration.EnableVAAPI)
@@ -894,3 +903,21 @@ func (m *Manager) validateAndConfigureCodecPreferences(config *Config) *Config {
894903
895904 return config
896905 }
906+
907+// validateColorScheme validates and normalizes the ColorScheme setting
908+func (m *Manager) validateColorScheme(config *Config) {
909+ switch config.Appearance.ColorScheme {
910+ case "prefer-dark", "prefer-light", "default", "":
911+ // Valid values - no changes needed
912+ // Empty string is treated the same as "default"
913+ if config.Appearance.ColorScheme == "" {
914+ config.Appearance.ColorScheme = "default"
915+ logging.Info("Config: ColorScheme not set, defaulting to 'default' (follows system)")
916+ }
917+ default:
918+ // Invalid value - warn and reset to default
919+ logging.Info(fmt.Sprintf("Config: Invalid color_scheme value '%s', valid values are: 'prefer-dark', 'prefer-light', 'default'. Resetting to 'default'",
920+ config.Appearance.ColorScheme))
921+ config.Appearance.ColorScheme = "default"
922+ }
923+}
@@ -124,6 +124,8 @@ type AppearanceConfig struct {
124 DefaultFontSize int `mapstructure:"default_font_size" yaml:"default_font_size"`124 DefaultFontSize int `mapstructure:"default_font_size" yaml:"default_font_size"`
125 LightPalette ColorPalette `mapstructure:"light_palette" yaml:"light_palette"`125 LightPalette ColorPalette `mapstructure:"light_palette" yaml:"light_palette"`
126 DarkPalette ColorPalette `mapstructure:"dark_palette" yaml:"dark_palette"`126 DarkPalette ColorPalette `mapstructure:"dark_palette" yaml:"dark_palette"`
127+ // ColorScheme controls the initial theme preference: "prefer-dark", "prefer-light", or "default" (follows system)
128+ ColorScheme string `mapstructure:"color_scheme" yaml:"color_scheme"`
127 }129 }
128 130
129 // ColorPalette contains semantic color tokens for light/dark themes.131 // ColorPalette contains semantic color tokens for light/dark themes.
@@ -545,6 +547,9 @@ func (m *Manager) Load() error {
545 // Validate and configure codec preferences547 // Validate and configure codec preferences
546 config = m.validateAndConfigureCodecPreferences(config)548 config = m.validateAndConfigureCodecPreferences(config)
547 549
550+ // Validate ColorScheme setting
551+ m.validateColorScheme(config)
552+
548 m.config = config553 m.config = config
549 return nil554 return nil
550 }555 }
@@ -643,6 +648,9 @@ func (m *Manager) reload() error {
643 // Validate and configure codec preferences648 // Validate and configure codec preferences
644 config = m.validateAndConfigureCodecPreferences(config)649 config = m.validateAndConfigureCodecPreferences(config)
645 650
651+ // Validate ColorScheme setting
652+ m.validateColorScheme(config)
653+
646 m.config = config654 m.config = config
647 return nil655 return nil
648 }656 }
@@ -713,6 +721,7 @@ func (m *Manager) setDefaults() {
713 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)721 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
714 m.viper.SetDefault("appearance.light_palette", defaults.Appearance.LightPalette)722 m.viper.SetDefault("appearance.light_palette", defaults.Appearance.LightPalette)
715 m.viper.SetDefault("appearance.dark_palette", defaults.Appearance.DarkPalette)723 m.viper.SetDefault("appearance.dark_palette", defaults.Appearance.DarkPalette)
724+ m.viper.SetDefault("appearance.color_scheme", defaults.Appearance.ColorScheme)
716 725
717 // Video acceleration defaults726 // Video acceleration defaults
718 m.viper.SetDefault("video_acceleration.enable_vaapi", defaults.VideoAcceleration.EnableVAAPI)727 m.viper.SetDefault("video_acceleration.enable_vaapi", defaults.VideoAcceleration.EnableVAAPI)
@@ -894,3 +903,21 @@ func (m *Manager) validateAndConfigureCodecPreferences(config *Config) *Config {
894 903
895 return config904 return config
896 }905 }
906+
907+// validateColorScheme validates and normalizes the ColorScheme setting
908+func (m *Manager) validateColorScheme(config *Config) {
909+ switch config.Appearance.ColorScheme {
910+ case "prefer-dark", "prefer-light", "default", "":
911+ // Valid values - no changes needed
912+ // Empty string is treated the same as "default"
913+ if config.Appearance.ColorScheme == "" {
914+ config.Appearance.ColorScheme = "default"
915+ logging.Info("Config: ColorScheme not set, defaulting to 'default' (follows system)")
916+ }
917+ default:
918+ // Invalid value - warn and reset to default
919+ logging.Info(fmt.Sprintf("Config: Invalid color_scheme value '%s', valid values are: 'prefer-dark', 'prefer-light', 'default'. Resetting to 'default'",
920+ config.Appearance.ColorScheme))
921+ config.Appearance.ColorScheme = "default"
922+ }
923+}
M internal/config/defaults.go +1 -0
@@ -112,6 +112,7 @@ func DefaultConfig() *Config {
112112 Accent: "#a8a8a8",
113113 Border: "#363636",
114114 },
115+ ColorScheme: "default", // default follows system theme
115116 },
116117 VideoAcceleration: VideoAccelerationConfig{
117118 EnableVAAPI: true,
@@ -112,6 +112,7 @@ func DefaultConfig() *Config {
112 Accent: "#a8a8a8",112 Accent: "#a8a8a8",
113 Border: "#363636",113 Border: "#363636",
114 },114 },
115+ ColorScheme: "default", // default follows system theme
115 },116 },
116 VideoAcceleration: VideoAccelerationConfig{117 VideoAcceleration: VideoAccelerationConfig{
117 EnableVAAPI: true,118 EnableVAAPI: true,
M pkg/webkit/helpers.go +23 -2
@@ -2,6 +2,7 @@ package webkit
22
33 import (
44 "github.com/diamondburned/gotk4/pkg/gdk/v4"
5+ "github.com/diamondburned/gotk4/pkg/gio/v2"
56 "github.com/diamondburned/gotk4/pkg/glib/v2"
67 "github.com/diamondburned/gotk4/pkg/gtk/v4"
78 )
@@ -239,10 +240,30 @@ func WidgetAllocation(w gtk.Widgetter) (x, y, width, height int) {
239240
240241 // PrefersDarkTheme checks if the system prefers dark theme
241242 func PrefersDarkTheme() bool {
243+ // First try to read GNOME's color-scheme setting (GTK4+)
244+ // Check if the schema exists before attempting to create GSettings object
245+ // to avoid panics on non-GNOME systems (e.g., Sway, Hyprland, etc.)
246+ var colorScheme string
247+ schemaSource := gio.SettingsSchemaSourceGetDefault()
248+ if schemaSource != nil {
249+ schema := schemaSource.Lookup("org.gnome.desktop.interface", true)
250+ if schema != nil {
251+ if gnomeSettings := gio.NewSettings("org.gnome.desktop.interface"); gnomeSettings != nil {
252+ colorScheme = gnomeSettings.String("color-scheme")
253+ }
254+ }
255+ }
256+
257+ switch colorScheme {
258+ case "prefer-dark":
259+ return true
260+ case "prefer-light":
261+ return false
262+ }
263+
264+ // Fallback to GTK's application-level preference
242265 settings := gtk.SettingsGetDefault()
243266 if settings != nil {
244- // In GTK4, check the color scheme preference
245- // The property is "gtk-application-prefer-dark-theme"
246267 obj := settings.Object
247268 if obj != nil {
248269 prop := obj.ObjectProperty("gtk-application-prefer-dark-theme")
@@ -2,6 +2,7 @@ package webkit
2 2
3 import (3 import (
4 "github.com/diamondburned/gotk4/pkg/gdk/v4"4 "github.com/diamondburned/gotk4/pkg/gdk/v4"
5+ "github.com/diamondburned/gotk4/pkg/gio/v2"
5 "github.com/diamondburned/gotk4/pkg/glib/v2"6 "github.com/diamondburned/gotk4/pkg/glib/v2"
6 "github.com/diamondburned/gotk4/pkg/gtk/v4"7 "github.com/diamondburned/gotk4/pkg/gtk/v4"
7 )8 )
@@ -239,10 +240,30 @@ func WidgetAllocation(w gtk.Widgetter) (x, y, width, height int) {
239 240
240 // PrefersDarkTheme checks if the system prefers dark theme241 // PrefersDarkTheme checks if the system prefers dark theme
241 func PrefersDarkTheme() bool {242 func PrefersDarkTheme() bool {
243+ // First try to read GNOME's color-scheme setting (GTK4+)
244+ // Check if the schema exists before attempting to create GSettings object
245+ // to avoid panics on non-GNOME systems (e.g., Sway, Hyprland, etc.)
246+ var colorScheme string
247+ schemaSource := gio.SettingsSchemaSourceGetDefault()
248+ if schemaSource != nil {
249+ schema := schemaSource.Lookup("org.gnome.desktop.interface", true)
250+ if schema != nil {
251+ if gnomeSettings := gio.NewSettings("org.gnome.desktop.interface"); gnomeSettings != nil {
252+ colorScheme = gnomeSettings.String("color-scheme")
253+ }
254+ }
255+ }
256+
257+ switch colorScheme {
258+ case "prefer-dark":
259+ return true
260+ case "prefer-light":
261+ return false
262+ }
263+
264+ // Fallback to GTK's application-level preference
242 settings := gtk.SettingsGetDefault()265 settings := gtk.SettingsGetDefault()
243 if settings != nil {266 if settings != nil {
244- // In GTK4, check the color scheme preference
245- // The property is "gtk-application-prefer-dark-theme"
246 obj := settings.Object267 obj := settings.Object
247 if obj != nil {268 if obj != nil {
248 prop := obj.ObjectProperty("gtk-application-prefer-dark-theme")269 prop := obj.ObjectProperty("gtk-application-prefer-dark-theme")
M pkg/webkit/user_content.go +18 -2
@@ -5,6 +5,7 @@ import (
55 "log"
66
77 "github.com/bnema/dumber/assets"
8+ "github.com/bnema/dumber/internal/config"
89 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
910 )
1011
@@ -39,7 +40,22 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
3940
4041 // Inject GTK theme detection SECOND, before color-scheme script
4142 // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set
42- prefersDark := PrefersDarkTheme()
43+ // Respect the ColorScheme config setting
44+ cfg := config.Get()
45+ var prefersDark bool
46+ switch cfg.Appearance.ColorScheme {
47+ case "prefer-dark":
48+ prefersDark = true
49+ log.Printf("[webkit] Using config-forced dark theme (color_scheme: prefer-dark)")
50+ case "prefer-light":
51+ prefersDark = false
52+ log.Printf("[webkit] Using config-forced light theme (color_scheme: prefer-light)")
53+ default:
54+ // "default" or empty - follow system GTK preference
55+ prefersDark = PrefersDarkTheme()
56+ log.Printf("[webkit] Using system GTK theme preference (color_scheme: %s)", cfg.Appearance.ColorScheme)
57+ }
58+
4359 gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark)
4460 ucm.AddScript(webkit.NewUserScript(
4561 gtkThemeScript,
@@ -48,7 +64,7 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
4864 nil,
4965 nil,
5066 ))
51- log.Printf("[webkit] Injected GTK theme preference: prefersDark=%t", prefersDark)
67+ log.Printf("[webkit] Injected theme preference: prefersDark=%t", prefersDark)
5268
5369 // Inject palette config SECOND, before GUI scripts
5470 // The GUI expects window.__dumber_palette = { "light": {...}, "dark": {...} }
@@ -5,6 +5,7 @@ import (
5 "log"5 "log"
6 6
7 "github.com/bnema/dumber/assets"7 "github.com/bnema/dumber/assets"
8+ "github.com/bnema/dumber/internal/config"
8 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"9 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
9 )10 )
10 11
@@ -39,7 +40,22 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
39 40
40 // Inject GTK theme detection SECOND, before color-scheme script41 // Inject GTK theme detection SECOND, before color-scheme script
41 // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set42 // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set
42- prefersDark := PrefersDarkTheme()43+ // Respect the ColorScheme config setting
44+ cfg := config.Get()
45+ var prefersDark bool
46+ switch cfg.Appearance.ColorScheme {
47+ case "prefer-dark":
48+ prefersDark = true
49+ log.Printf("[webkit] Using config-forced dark theme (color_scheme: prefer-dark)")
50+ case "prefer-light":
51+ prefersDark = false
52+ log.Printf("[webkit] Using config-forced light theme (color_scheme: prefer-light)")
53+ default:
54+ // "default" or empty - follow system GTK preference
55+ prefersDark = PrefersDarkTheme()
56+ log.Printf("[webkit] Using system GTK theme preference (color_scheme: %s)", cfg.Appearance.ColorScheme)
57+ }
58+
43 gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark)59 gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark)
44 ucm.AddScript(webkit.NewUserScript(60 ucm.AddScript(webkit.NewUserScript(
45 gtkThemeScript,61 gtkThemeScript,
@@ -48,7 +64,7 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
48 nil,64 nil,
49 nil,65 nil,
50 ))66 ))
51- log.Printf("[webkit] Injected GTK theme preference: prefersDark=%t", prefersDark)67+ log.Printf("[webkit] Injected theme preference: prefersDark=%t", prefersDark)
52 68
53 // Inject palette config SECOND, before GUI scripts69 // Inject palette config SECOND, before GUI scripts
54 // The GUI expects window.__dumber_palette = { "light": {...}, "dark": {...} }70 // The GUI expects window.__dumber_palette = { "light": {...}, "dark": {...} }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close