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 ( | |||
| 15 | 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 | 19 | // It must be injected at document start, after __dumber_gtk_prefers_dark is set. | |
| 20 | 20 | const darkModeScript = `(function() { | |
| 21 | 21 | const prefersDark = window.__dumber_gtk_prefers_dark || false; | |
| 22 | 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 | 33 | // Inject color-scheme meta tag | |
| 25 | 34 | const meta = document.createElement('meta'); | |
| 26 | 35 | 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 tag | 33 | // 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "os" | |
| 5 | + | "os/exec" | |
| 5 | 6 | "strings" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| @@ -10,7 +11,8 @@ import ( | |||
| 10 | 11 | // DetectSystemDarkMode checks if the system prefers dark mode. | |
| 11 | 12 | // It checks multiple sources in order of preference: | |
| 12 | 13 | // 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 | |
| 14 | 16 | // Returns true if dark mode is preferred. | |
| 15 | 17 | func DetectSystemDarkMode() bool { | |
| 16 | 18 | // Check GTK_THEME environment variable first | |
| @@ -23,6 +25,17 @@ func DetectSystemDarkMode() bool { | |||
| 23 | 25 | 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 | 39 | // Try GTK Settings | |
| 27 | 40 | settings := gtk.SettingsGetDefault() | |
| 28 | 41 | if settings != nil { | |
| @@ -35,6 +48,20 @@ func DetectSystemDarkMode() bool { | |||
| 35 | 48 | 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 | 65 | // ResolveColorScheme determines the effective dark mode preference. | |
| 39 | 66 | // It takes the config value ("prefer-dark", "prefer-light", "default") | |
| 40 | 67 | // 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 property | 14 | + | // 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 first | 18 | // Check GTK_THEME environment variable first | ||
| @@ -23,6 +25,17 @@ func DetectSystemDarkMode() bool { | |||||
| 23 | return false | 25 | 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 Settings | 39 | // 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 true | 48 | 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. | ||