@@ -7,6 +7,84 @@ import (
77 "github.com/wailsapp/wails/v3/pkg/events"
88)
99
10+ const (
11+ portalBusName = "org.freedesktop.portal.Desktop"
12+ portalObjectPath = "/org/freedesktop/portal/desktop"
13+ portalSettingsIface = "org.freedesktop.portal.Settings"
14+
15+ // appearanceNamespace is the standardised namespace every portal
16+ // implementation publishes; it is the only one read or watched. GNOME also
17+ // mirrors the preference under org.gnome.desktop.interface as a string, but
18+ // honouring that in the signal filter alone would be inert: the handler
19+ // resolves through portalColorScheme, which speaks only this namespace.
20+ appearanceNamespace = "org.freedesktop.appearance"
21+ colorSchemeKey = "color-scheme"
22+
23+ colorSchemePreferDark = 1
24+ )
25+
26+ // isDarkMode reports the desktop colour-scheme preference, read from the
27+ // freedesktop Settings portal on every call.
28+ //
29+ // Read on demand rather than served from state maintained by
30+ // monitorThemeChanges: that monitor is started per backend, so any cache it
31+ // owns is only as correct as its startup wiring, and a cache fed from
32+ // SettingChanged payloads reports light on every desktop until the first signal
33+ // arrives. An on-demand read is right whether or not the monitor is running.
34+ // Callers that need this on a hot path should cache it themselves.
35+ func (a * linuxApp ) isDarkMode () bool {
36+ scheme , ok := portalColorScheme ()
37+ return ok && scheme == colorSchemePreferDark
38+ }
39+
40+ // portalColorScheme reads org.freedesktop.appearance color-scheme: 0 is no
41+ // preference, 1 prefers dark, 2 prefers light. ok is false when the portal is
42+ // unreachable or the value is not the documented type.
43+ func portalColorScheme () (uint32 , bool ) {
44+ conn , err := dbus .SessionBus ()
45+ if err != nil {
46+ return 0 , false
47+ }
48+
49+ obj := conn .Object (portalBusName , portalObjectPath )
50+ call := obj .Call (portalSettingsIface + ".Read" , 0 , appearanceNamespace , colorSchemeKey )
51+ if call .Err != nil {
52+ return 0 , false
53+ }
54+
55+ var outer dbus.Variant
56+ if err := call .Store (& outer ); err != nil {
57+ return 0 , false
58+ }
59+ // Portal v1 Read double-wraps the value; other implementations, and ReadOne,
60+ // return it singly wrapped. Accept both, because rejecting one shape here
61+ // silently reports light -- the failure this whole path exists to remove.
62+ if inner , ok := outer .Value ().(dbus.Variant ); ok {
63+ scheme , ok := inner .Value ().(uint32 )
64+ return scheme , ok
65+ }
66+ scheme , ok := outer .Value ().(uint32 )
67+ return scheme , ok
68+ }
69+
70+ // isColorSchemeChange reports whether a signal is a colour-scheme
71+ // SettingChanged in the standardised appearance namespace.
72+ func isColorSchemeChange (sig * dbus.Signal ) bool {
73+ if sig .Name != portalSettingsIface + ".SettingChanged" {
74+ return false
75+ }
76+ if len (sig .Body ) < 2 {
77+ return false
78+ }
79+ namespace , _ := sig .Body [0 ].(string )
80+ key , _ := sig .Body [1 ].(string )
81+ return namespace == appearanceNamespace && key == colorSchemeKey
82+ }
83+
84+ // monitorThemeChanges emits Linux.SystemThemeChanged when the desktop colour
85+ // scheme changes. The portal is re-read rather than the signal payload trusted,
86+ // so the emitted value always agrees with isDarkMode regardless of which
87+ // namespace fired and what type it carried.
1088func (a * linuxApp ) monitorThemeChanges () {
1189 go func () {
1290 defer handlePanic ()
@@ -21,7 +99,10 @@ func (a *linuxApp) monitorThemeChanges() {
2199 defer conn .Close ()
22100
23101 if err = conn .AddMatchSignal (
24- dbus .WithMatchObjectPath ("/org/freedesktop/portal/desktop" ),
102+ dbus .WithMatchSender (portalBusName ),
103+ dbus .WithMatchObjectPath (portalObjectPath ),
104+ dbus .WithMatchInterface (portalSettingsIface ),
105+ dbus .WithMatchMember ("SettingChanged" ),
25106 ); err != nil {
26107 a .parent .warning (
27108 "[WARNING] Failed to subscribe to portal SettingChanged; theme changes will not fire: %v" ,
@@ -33,40 +114,20 @@ func (a *linuxApp) monitorThemeChanges() {
33114 c := make (chan * dbus.Signal , 10 )
34115 conn .Signal (c )
35116
36- getTheme := func (body []interface {}) (string , bool ) {
37- if len (body ) < 3 {
38- return "" , false
39- }
40- if entry , ok := body [0 ].(string ); ! ok || entry != "org.gnome.desktop.interface" {
41- return "" , false
42- }
43- if entry , ok := body [1 ].(string ); ! ok || entry != "color-scheme" {
44- return "" , false
45- }
46- variant , ok := body [2 ].(dbus.Variant )
47- if ! ok {
48- return "" , false
49- }
50- value , ok := variant .Value ().(string )
51- if ! ok {
52- return "" , false
53- }
54- return value , true
55- }
56-
117+ last := a .isDarkMode ()
57118 for v := range c {
58- theme , ok := getTheme (v .Body )
59- if ! ok {
119+ if ! isColorSchemeChange (v ) {
60120 continue
61121 }
62-
63- if theme != a .theme {
64- a .theme = theme
65- event := newApplicationEvent (events .Linux .SystemThemeChanged )
66- event .Context ().setIsDarkMode (a .isDarkMode ())
67- applicationEvents <- event
122+ dark := a .isDarkMode ()
123+ if dark == last {
124+ continue
68125 }
126+ last = dark
69127
128+ event := newApplicationEvent (events .Linux .SystemThemeChanged )
129+ event .Context ().setIsDarkMode (dark )
130+ applicationEvents <- event
70131 }
71132 }()
72133}
0 commit comments