detached ffd97bd

fix(theme): add gsettings color-scheme detection for modern GTK/GNOME

commit
ffd97bd0b00d82dd6dd88538100c179c5bd84ad1
author
brice · · 2025-12-17 17:14 UTC
parent
25a9d2e
signature
unsigned
2 files · +38 -2
M internal/infrastructure/webkit/injector.go +10 -1
@@ -15,12 +15,21 @@ const (
1515 MessageHandlerName = "dumber"
1616 )
1717
18-// darkModeScript patches window.matchMedia to respect GTK's dark mode preference.
18+// darkModeScript patches window.matchMedia and sets theme class on <html>.
1919 // It must be injected at document start, after __dumber_gtk_prefers_dark is set.
2020 const darkModeScript = `(function() {
2121 const prefersDark = window.__dumber_gtk_prefers_dark || false;
2222 const originalMatchMedia = window.matchMedia.bind(window);
2323
24+ // Apply dark/light class to document element for CSS theming
25+ if (prefersDark) {
26+ document.documentElement.classList.add('dark');
27+ document.documentElement.classList.remove('light');
28+ } else {
29+ document.documentElement.classList.add('light');
30+ document.documentElement.classList.remove('dark');
31+ }
32+
2433 // Inject color-scheme meta tag
2534 const meta = document.createElement('meta');
2635 meta.name = 'color-scheme';
@@ -15,12 +15,21 @@ const (
15 MessageHandlerName = "dumber"15 MessageHandlerName = "dumber"
16 )16 )
17 17
18-// darkModeScript patches window.matchMedia to respect GTK's dark mode preference.18+// darkModeScript patches window.matchMedia and sets theme class on <html>.
19 // It must be injected at document start, after __dumber_gtk_prefers_dark is set.19 // It must be injected at document start, after __dumber_gtk_prefers_dark is set.
20 const darkModeScript = `(function() {20 const darkModeScript = `(function() {
21 const prefersDark = window.__dumber_gtk_prefers_dark || false;21 const prefersDark = window.__dumber_gtk_prefers_dark || false;
22 const originalMatchMedia = window.matchMedia.bind(window);22 const originalMatchMedia = window.matchMedia.bind(window);
23 23
24+ // Apply dark/light class to document element for CSS theming
25+ if (prefersDark) {
26+ document.documentElement.classList.add('dark');
27+ document.documentElement.classList.remove('light');
28+ } else {
29+ document.documentElement.classList.add('light');
30+ document.documentElement.classList.remove('dark');
31+ }
32+
24 // Inject color-scheme meta tag33 // Inject color-scheme meta tag
25 const meta = document.createElement('meta');34 const meta = document.createElement('meta');
26 meta.name = 'color-scheme';35 meta.name = 'color-scheme';
M internal/ui/theme/system.go +28 -1
@@ -2,6 +2,7 @@ package theme
22
33 import (
44 "os"
5+ "os/exec"
56 "strings"
67
78 "github.com/jwijenbergh/puregotk/v4/gtk"
@@ -10,7 +11,8 @@ import (
1011 // DetectSystemDarkMode checks if the system prefers dark mode.
1112 // It checks multiple sources in order of preference:
1213 // 1. GTK_THEME environment variable (contains "dark")
13-// 2. GTK Settings gtk-application-prefer-dark-theme property
14+// 2. GNOME gsettings color-scheme (prefer-dark/prefer-light)
15+// 3. GTK Settings gtk-application-prefer-dark-theme property
1416 // Returns true if dark mode is preferred.
1517 func DetectSystemDarkMode() bool {
1618 // Check GTK_THEME environment variable first
@@ -23,6 +25,17 @@ func DetectSystemDarkMode() bool {
2325 return false
2426 }
2527
28+ // Check GNOME gsettings color-scheme (modern GNOME/GTK4 method)
29+ if colorScheme := getGsettingsColorScheme(); colorScheme != "" {
30+ switch colorScheme {
31+ case "prefer-dark":
32+ return true
33+ case "prefer-light":
34+ return false
35+ // "default" falls through to next check
36+ }
37+ }
38+
2639 // Try GTK Settings
2740 settings := gtk.SettingsGetDefault()
2841 if settings != nil {
@@ -35,6 +48,20 @@ func DetectSystemDarkMode() bool {
3548 return true
3649 }
3750
51+// getGsettingsColorScheme queries GNOME gsettings for the color-scheme preference.
52+// Returns "prefer-dark", "prefer-light", "default", or empty string on error.
53+func getGsettingsColorScheme() string {
54+ cmd := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme")
55+ output, err := cmd.Output()
56+ if err != nil {
57+ return ""
58+ }
59+ // Output is like "'prefer-dark'\n", strip quotes and whitespace
60+ result := strings.TrimSpace(string(output))
61+ result = strings.Trim(result, "'\"")
62+ return result
63+}
64+
3865 // ResolveColorScheme determines the effective dark mode preference.
3966 // It takes the config value ("prefer-dark", "prefer-light", "default")
4067 // and resolves "default" to the system preference.
@@ -2,6 +2,7 @@ package theme
2 2
3 import (3 import (
4 "os"4 "os"
5+ "os/exec"
5 "strings"6 "strings"
6 7
7 "github.com/jwijenbergh/puregotk/v4/gtk"8 "github.com/jwijenbergh/puregotk/v4/gtk"
@@ -10,7 +11,8 @@ import (
10 // DetectSystemDarkMode checks if the system prefers dark mode.11 // DetectSystemDarkMode checks if the system prefers dark mode.
11 // It checks multiple sources in order of preference:12 // It checks multiple sources in order of preference:
12 // 1. GTK_THEME environment variable (contains "dark")13 // 1. GTK_THEME environment variable (contains "dark")
13-// 2. GTK Settings gtk-application-prefer-dark-theme property14+// 2. GNOME gsettings color-scheme (prefer-dark/prefer-light)
15+// 3. GTK Settings gtk-application-prefer-dark-theme property
14 // Returns true if dark mode is preferred.16 // Returns true if dark mode is preferred.
15 func DetectSystemDarkMode() bool {17 func DetectSystemDarkMode() bool {
16 // Check GTK_THEME environment variable first18 // Check GTK_THEME environment variable first
@@ -23,6 +25,17 @@ func DetectSystemDarkMode() bool {
23 return false25 return false
24 }26 }
25 27
28+ // Check GNOME gsettings color-scheme (modern GNOME/GTK4 method)
29+ if colorScheme := getGsettingsColorScheme(); colorScheme != "" {
30+ switch colorScheme {
31+ case "prefer-dark":
32+ return true
33+ case "prefer-light":
34+ return false
35+ // "default" falls through to next check
36+ }
37+ }
38+
26 // Try GTK Settings39 // Try GTK Settings
27 settings := gtk.SettingsGetDefault()40 settings := gtk.SettingsGetDefault()
28 if settings != nil {41 if settings != nil {
@@ -35,6 +48,20 @@ func DetectSystemDarkMode() bool {
35 return true48 return true
36 }49 }
37 50
51+// getGsettingsColorScheme queries GNOME gsettings for the color-scheme preference.
52+// Returns "prefer-dark", "prefer-light", "default", or empty string on error.
53+func getGsettingsColorScheme() string {
54+ cmd := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme")
55+ output, err := cmd.Output()
56+ if err != nil {
57+ return ""
58+ }
59+ // Output is like "'prefer-dark'\n", strip quotes and whitespace
60+ result := strings.TrimSpace(string(output))
61+ result = strings.Trim(result, "'\"")
62+ return result
63+}
64+
38 // ResolveColorScheme determines the effective dark mode preference.65 // ResolveColorScheme determines the effective dark mode preference.
39 // It takes the config value ("prefer-dark", "prefer-light", "default")66 // It takes the config value ("prefer-dark", "prefer-light", "default")
40 // and resolves "default" to the system preference.67 // and resolves "default" to the system preference.

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