Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 3 additions & 69 deletions v3/pkg/application/application_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"strings"
"sync"

"github.com/godbus/dbus/v5"
"github.com/wailsapp/wails/v3/internal/operatingsystem"
"github.com/wailsapp/wails/v3/pkg/events"
)
Expand Down Expand Up @@ -70,8 +69,6 @@ type linuxApp struct {
windowMap map[windowPointer]uint
windowMapLock sync.Mutex

theme string

icon pointer
}

Expand All @@ -93,8 +90,9 @@ func (a *linuxApp) run() error {
}
})
a.setupCommonEvents()
// Theme changes are already monitored by listenForSystemThemeChanges via init();
// it uses the portal-standard org.freedesktop.appearance namespace.
// Started here, not from init(): init() is not part of the platformApp
// interface and nothing calls it, so a monitor started there never runs.
a.monitorThemeChanges()
a.monitorPowerEvents()
return appRun(a.application)
}
Expand Down Expand Up @@ -154,41 +152,6 @@ func (a *linuxApp) init(_ *App, options Options) {
if options.Icon != nil {
a.setIcon(options.Icon)
}

go listenForSystemThemeChanges(a)
}

func listenForSystemThemeChanges(a *linuxApp) {
conn, err := dbus.SessionBus()
if err != nil {
a.parent.error("failed to connect to session bus: %v", err)
return
}

if err = conn.AddMatchSignal(
dbus.WithMatchInterface("org.freedesktop.portal.Settings"),
dbus.WithMatchMember("SettingChanged"),
); err != nil {
return
}

c := make(chan *dbus.Signal, 10)
conn.Signal(c)

for s := range c {
if len(s.Body) < 3 {
continue
}
namespace, ok := s.Body[0].(string)
if !ok || namespace != "org.freedesktop.appearance" {
continue
}
key, ok := s.Body[1].(string)
if !ok || key != "color-scheme" {
continue
}
processApplicationEvent(C.uint(events.Linux.SystemThemeChanged), nil)
}
}

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

func (a *linuxApp) isDarkMode() bool {
conn, err := dbus.SessionBus()
if err != nil {
return false
}

obj := conn.Object("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop")
call := obj.Call("org.freedesktop.portal.Settings.Read", 0, "org.freedesktop.appearance", "color-scheme")
if call.Err != nil {
return false
}

var result dbus.Variant
if err := call.Store(&result); err != nil {
return false
}

innerVariant, ok := result.Value().(dbus.Variant)
if !ok {
return false
}
colorScheme, ok := innerVariant.Value().(uint32)
if !ok {
return false
}

return colorScheme == 1
}

func (a *linuxApp) getAccentColor() string {
return "rgb(0,122,255)"
}
Expand Down
121 changes: 91 additions & 30 deletions v3/pkg/application/application_linux_dbus.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,84 @@ import (
"github.com/wailsapp/wails/v3/pkg/events"
)

const (
portalBusName = "org.freedesktop.portal.Desktop"
portalObjectPath = "/org/freedesktop/portal/desktop"
portalSettingsIface = "org.freedesktop.portal.Settings"

// appearanceNamespace is the standardised namespace every portal
// implementation publishes; it is the only one read or watched. GNOME also
// mirrors the preference under org.gnome.desktop.interface as a string, but
// honouring that in the signal filter alone would be inert: the handler
// resolves through portalColorScheme, which speaks only this namespace.
appearanceNamespace = "org.freedesktop.appearance"
colorSchemeKey = "color-scheme"

colorSchemePreferDark = 1
)

// isDarkMode reports the desktop colour-scheme preference, read from the
// freedesktop Settings portal on every call.
//
// Read on demand rather than served from state maintained by
// monitorThemeChanges: that monitor is started per backend, so any cache it
// owns is only as correct as its startup wiring, and a cache fed from
// SettingChanged payloads reports light on every desktop until the first signal
// arrives. An on-demand read is right whether or not the monitor is running.
// Callers that need this on a hot path should cache it themselves.
func (a *linuxApp) isDarkMode() bool {
scheme, ok := portalColorScheme()
return ok && scheme == colorSchemePreferDark
}

// portalColorScheme reads org.freedesktop.appearance color-scheme: 0 is no
// preference, 1 prefers dark, 2 prefers light. ok is false when the portal is
// unreachable or the value is not the documented type.
func portalColorScheme() (uint32, bool) {
conn, err := dbus.SessionBus()
if err != nil {
return 0, false
}

obj := conn.Object(portalBusName, portalObjectPath)
call := obj.Call(portalSettingsIface+".Read", 0, appearanceNamespace, colorSchemeKey)
if call.Err != nil {
return 0, false
}

var outer dbus.Variant
if err := call.Store(&outer); err != nil {
return 0, false
}
// Portal v1 Read double-wraps the value; other implementations, and ReadOne,
// return it singly wrapped. Accept both, because rejecting one shape here
// silently reports light -- the failure this whole path exists to remove.
if inner, ok := outer.Value().(dbus.Variant); ok {
scheme, ok := inner.Value().(uint32)
return scheme, ok
}
scheme, ok := outer.Value().(uint32)
return scheme, ok
}

// isColorSchemeChange reports whether a signal is a colour-scheme
// SettingChanged in the standardised appearance namespace.
func isColorSchemeChange(sig *dbus.Signal) bool {
if sig.Name != portalSettingsIface+".SettingChanged" {
return false
}
if len(sig.Body) < 2 {
return false
}
namespace, _ := sig.Body[0].(string)
key, _ := sig.Body[1].(string)
return namespace == appearanceNamespace && key == colorSchemeKey
}

// monitorThemeChanges emits Linux.SystemThemeChanged when the desktop colour
// scheme changes. The portal is re-read rather than the signal payload trusted,
// so the emitted value always agrees with isDarkMode regardless of which
// namespace fired and what type it carried.
func (a *linuxApp) monitorThemeChanges() {
go func() {
defer handlePanic()
Expand All @@ -21,7 +99,10 @@ func (a *linuxApp) monitorThemeChanges() {
defer conn.Close()

if err = conn.AddMatchSignal(
dbus.WithMatchObjectPath("/org/freedesktop/portal/desktop"),
dbus.WithMatchSender(portalBusName),
dbus.WithMatchObjectPath(portalObjectPath),
dbus.WithMatchInterface(portalSettingsIface),
dbus.WithMatchMember("SettingChanged"),
); err != nil {
a.parent.warning(
"[WARNING] Failed to subscribe to portal SettingChanged; theme changes will not fire: %v",
Expand All @@ -33,40 +114,20 @@ func (a *linuxApp) monitorThemeChanges() {
c := make(chan *dbus.Signal, 10)
conn.Signal(c)

getTheme := func(body []interface{}) (string, bool) {
if len(body) < 3 {
return "", false
}
if entry, ok := body[0].(string); !ok || entry != "org.gnome.desktop.interface" {
return "", false
}
if entry, ok := body[1].(string); !ok || entry != "color-scheme" {
return "", false
}
variant, ok := body[2].(dbus.Variant)
if !ok {
return "", false
}
value, ok := variant.Value().(string)
if !ok {
return "", false
}
return value, true
}

last := a.isDarkMode()
for v := range c {
theme, ok := getTheme(v.Body)
if !ok {
if !isColorSchemeChange(v) {
continue
}

if theme != a.theme {
a.theme = theme
event := newApplicationEvent(events.Linux.SystemThemeChanged)
event.Context().setIsDarkMode(a.isDarkMode())
applicationEvents <- event
dark := a.isDarkMode()
if dark == last {
continue
}
last = dark

event := newApplicationEvent(events.Linux.SystemThemeChanged)
event.Context().setIsDarkMode(dark)
applicationEvents <- event
}
}()
}
Expand Down
6 changes: 0 additions & 6 deletions v3/pkg/application/application_linux_gtk3.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,6 @@ type linuxApp struct {
windowMap map[windowPointer]uint
windowMapLock sync.Mutex

theme string

icon pointer
}

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

func (a *linuxApp) isDarkMode() bool {
return strings.Contains(a.theme, "dark")
}

func (a *linuxApp) getAccentColor() string {
// Linux doesn't have a unified system accent color API
// Return a default blue color
Expand Down
6 changes: 0 additions & 6 deletions v3/pkg/application/linux_cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,6 @@ func activateLinux(data pointer) {
//export processApplicationEvent
func processApplicationEvent(eventID C.uint, data pointer) {
event := newApplicationEvent(events.ApplicationEventType(eventID))

switch event.Id {
case uint(events.Linux.SystemThemeChanged):
isDark := globalApplication.Env.IsDarkMode()
event.Context().setIsDarkMode(isDark)
}
applicationEvents <- event
}

Expand Down
5 changes: 0 additions & 5 deletions v3/pkg/application/linux_cgo_gtk3.go
Original file line number Diff line number Diff line change
Expand Up @@ -598,11 +598,6 @@ func processApplicationEvent(eventID C.uint, data pointer) {
// }
//}

switch event.Id {
case uint(events.Linux.SystemThemeChanged):
isDark := globalApplication.Env.IsDarkMode()
event.Context().setIsDarkMode(isDark)
}
applicationEvents <- event
}

Expand Down
Loading