Skip to content

Commit daba333

Browse files
committed
System theme switching bug on Linux
1 parent 9f2fbd3 commit daba333

5 files changed

Lines changed: 94 additions & 116 deletions

File tree

v3/pkg/application/application_linux.go

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"strings"
2323
"sync"
2424

25-
"github.com/godbus/dbus/v5"
2625
"github.com/wailsapp/wails/v3/internal/operatingsystem"
2726
"github.com/wailsapp/wails/v3/pkg/events"
2827
)
@@ -71,8 +70,6 @@ type linuxApp struct {
7170
windowMap map[windowPointer]uint
7271
windowMapLock sync.Mutex
7372

74-
theme string
75-
7673
icon pointer
7774
}
7875

@@ -116,8 +113,9 @@ func (a *linuxApp) run() error {
116113
}
117114
})
118115
a.setupCommonEvents()
119-
// Theme changes are already monitored by listenForSystemThemeChanges via init();
120-
// it uses the portal-standard org.freedesktop.appearance namespace.
116+
// Started here, not from init(): init() is not part of the platformApp
117+
// interface and nothing calls it, so a monitor started there never runs.
118+
a.monitorThemeChanges()
121119
a.monitorPowerEvents()
122120
return appRun(a.application)
123121
}
@@ -177,41 +175,6 @@ func (a *linuxApp) init(_ *App, options Options) {
177175
if options.Icon != nil {
178176
a.setIcon(options.Icon)
179177
}
180-
181-
go listenForSystemThemeChanges(a)
182-
}
183-
184-
func listenForSystemThemeChanges(a *linuxApp) {
185-
conn, err := dbus.SessionBus()
186-
if err != nil {
187-
a.parent.error("failed to connect to session bus: %v", err)
188-
return
189-
}
190-
191-
if err = conn.AddMatchSignal(
192-
dbus.WithMatchInterface("org.freedesktop.portal.Settings"),
193-
dbus.WithMatchMember("SettingChanged"),
194-
); err != nil {
195-
return
196-
}
197-
198-
c := make(chan *dbus.Signal, 10)
199-
conn.Signal(c)
200-
201-
for s := range c {
202-
if len(s.Body) < 3 {
203-
continue
204-
}
205-
namespace, ok := s.Body[0].(string)
206-
if !ok || namespace != "org.freedesktop.appearance" {
207-
continue
208-
}
209-
key, ok := s.Body[1].(string)
210-
if !ok || key != "color-scheme" {
211-
continue
212-
}
213-
processApplicationEvent(C.uint(events.Linux.SystemThemeChanged), nil)
214-
}
215178
}
216179

217180
func (a *linuxApp) registerWindow(window pointer, id uint) {
@@ -302,35 +265,6 @@ func getIconBytes(iconName string) ([]byte, error) {
302265
return nil, fmt.Errorf("icon lookup is not currently implemented for the GTK4 build path; build with -tags gtk3 for the legacy implementation")
303266
}
304267

305-
func (a *linuxApp) isDarkMode() bool {
306-
conn, err := dbus.SessionBus()
307-
if err != nil {
308-
return false
309-
}
310-
311-
obj := conn.Object("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop")
312-
call := obj.Call("org.freedesktop.portal.Settings.Read", 0, "org.freedesktop.appearance", "color-scheme")
313-
if call.Err != nil {
314-
return false
315-
}
316-
317-
var result dbus.Variant
318-
if err := call.Store(&result); err != nil {
319-
return false
320-
}
321-
322-
innerVariant, ok := result.Value().(dbus.Variant)
323-
if !ok {
324-
return false
325-
}
326-
colorScheme, ok := innerVariant.Value().(uint32)
327-
if !ok {
328-
return false
329-
}
330-
331-
return colorScheme == 1
332-
}
333-
334268
func (a *linuxApp) getAccentColor() string {
335269
return "rgb(0,122,255)"
336270
}

v3/pkg/application/application_linux_dbus.go

Lines changed: 91 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
1088
func (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
}

v3/pkg/application/application_linux_gtk3.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,6 @@ type linuxApp struct {
8787
windowMap map[windowPointer]uint
8888
windowMapLock sync.Mutex
8989

90-
theme string
91-
9290
icon pointer
9391
}
9492

@@ -216,10 +214,6 @@ func (a *linuxApp) registerWindow(window pointer, id uint) {
216214
a.windowMapLock.Unlock()
217215
}
218216

219-
func (a *linuxApp) isDarkMode() bool {
220-
return strings.Contains(a.theme, "dark")
221-
}
222-
223217
func (a *linuxApp) getAccentColor() string {
224218
// Linux doesn't have a unified system accent color API
225219
// Return a default blue color

v3/pkg/application/linux_cgo.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,6 @@ func activateLinux(data pointer) {
118118
//export processApplicationEvent
119119
func processApplicationEvent(eventID C.uint, data pointer) {
120120
event := newApplicationEvent(events.ApplicationEventType(eventID))
121-
122-
switch event.Id {
123-
case uint(events.Linux.SystemThemeChanged):
124-
isDark := globalApplication.Env.IsDarkMode()
125-
event.Context().setIsDarkMode(isDark)
126-
}
127121
applicationEvents <- event
128122
}
129123

v3/pkg/application/linux_cgo_gtk3.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -598,11 +598,6 @@ func processApplicationEvent(eventID C.uint, data pointer) {
598598
// }
599599
//}
600600

601-
switch event.Id {
602-
case uint(events.Linux.SystemThemeChanged):
603-
isDark := globalApplication.Env.IsDarkMode()
604-
event.Context().setIsDarkMode(isDark)
605-
}
606601
applicationEvents <- event
607602
}
608603

0 commit comments

Comments
 (0)