From 8a85ab06a1276df611d4748373240049995eb537 Mon Sep 17 00:00:00 2001 From: bbedward Date: Sat, 4 Jul 2026 20:42:33 -0400 Subject: [PATCH] portal: prevent feedback loop with the gtk4 hack to make it react to color scheme changes --- core/internal/matugen/matugen.go | 14 +++++++ core/internal/server/freedesktop/manager.go | 40 +++++++++++++++++- .../server/freedesktop/manager_test.go | 42 +++++++++++++++++++ core/internal/server/freedesktop/types.go | 8 ++++ core/internal/server/server.go | 2 + quickshell/Services/PortalService.qml | 41 +++++++++++++----- 6 files changed, 136 insertions(+), 11 deletions(-) diff --git a/core/internal/matugen/matugen.go b/core/internal/matugen/matugen.go index 063275126..a5b78e6c0 100644 --- a/core/internal/matugen/matugen.go +++ b/core/internal/matugen/matugen.go @@ -912,6 +912,18 @@ func refreshGTK(mode ColorMode) { } } +var colorSchemeEchoHook func(scheme string) + +func SetColorSchemeEchoHook(hook func(scheme string)) { + colorSchemeEchoHook = hook +} + +func expectColorSchemeEcho(scheme string) { + if colorSchemeEchoHook != nil { + colorSchemeEchoHook(scheme) + } +} + func refreshGTK4() { output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme") if err != nil { @@ -926,11 +938,13 @@ func refreshGTK4() { toggle = "prefer-dark" } + expectColorSchemeEcho(toggle) if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil { log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err) return } time.Sleep(50 * time.Millisecond) + expectColorSchemeEcho(current) if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil { log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err) } diff --git a/core/internal/server/freedesktop/manager.go b/core/internal/server/freedesktop/manager.go index 1489245e4..c1d550191 100644 --- a/core/internal/server/freedesktop/manager.go +++ b/core/internal/server/freedesktop/manager.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "sync" + "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil" @@ -100,6 +101,41 @@ func (m *Manager) initializeSettings() error { return nil } +// ExpectColorSchemeEcho registers a self-write so the watcher swallows its SettingChanged echo instead of forwarding it as an external change. +func (m *Manager) ExpectColorSchemeEcho(scheme string) { + var value uint32 + switch scheme { + case "prefer-dark": + value = 1 + case "prefer-light": + value = 2 + } + m.selfEchoMu.Lock() + m.selfEchoes = append(m.selfEchoes, colorSchemeEcho{value: value, expires: time.Now().Add(10 * time.Second)}) + m.selfEchoMu.Unlock() +} + +func (m *Manager) consumeSelfEcho(value uint32) bool { + m.selfEchoMu.Lock() + defer m.selfEchoMu.Unlock() + + now := time.Now() + kept := m.selfEchoes[:0] + consumed := false + for _, echo := range m.selfEchoes { + if now.After(echo.expires) { + continue + } + if !consumed && echo.value == value { + consumed = true + continue + } + kept = append(kept, echo) + } + m.selfEchoes = kept + return consumed +} + func (m *Manager) watchSettingsChanges() { conn, err := dbus.ConnectSessionBus() if err != nil { @@ -142,13 +178,15 @@ func (m *Manager) watchSettingsChanges() { continue } + selfInflicted := m.consumeSelfEcho(colorScheme) + m.stateMutex.Lock() changed := m.state.Settings.ColorScheme != colorScheme || !m.state.Settings.Available m.state.Settings.ColorScheme = colorScheme m.state.Settings.Available = true m.stateMutex.Unlock() - if changed { + if changed && !selfInflicted { m.NotifySubscribers() } } diff --git a/core/internal/server/freedesktop/manager_test.go b/core/internal/server/freedesktop/manager_test.go index b3617f5e4..a37e2ec39 100644 --- a/core/internal/server/freedesktop/manager_test.go +++ b/core/internal/server/freedesktop/manager_test.go @@ -141,3 +141,45 @@ func TestManager_SettingsState_Modification(t *testing.T) { original := manager.GetState() assert.Equal(t, uint32(0), original.Settings.ColorScheme) } + +func TestManager_SelfEcho_ConsumesRegisteredWrites(t *testing.T) { + manager := &Manager{state: &FreedeskState{}} + + manager.ExpectColorSchemeEcho("prefer-dark") + manager.ExpectColorSchemeEcho("default") + + assert.True(t, manager.consumeSelfEcho(1)) + assert.True(t, manager.consumeSelfEcho(0)) + assert.False(t, manager.consumeSelfEcho(1)) + assert.False(t, manager.consumeSelfEcho(0)) +} + +func TestManager_SelfEcho_ExternalChangePassesThrough(t *testing.T) { + manager := &Manager{state: &FreedeskState{}} + + manager.ExpectColorSchemeEcho("prefer-dark") + + assert.False(t, manager.consumeSelfEcho(2)) + assert.True(t, manager.consumeSelfEcho(1)) +} + +func TestManager_SelfEcho_ConsumesOnePerRegistration(t *testing.T) { + manager := &Manager{state: &FreedeskState{}} + + manager.ExpectColorSchemeEcho("prefer-dark") + manager.ExpectColorSchemeEcho("prefer-dark") + + assert.True(t, manager.consumeSelfEcho(1)) + assert.True(t, manager.consumeSelfEcho(1)) + assert.False(t, manager.consumeSelfEcho(1)) +} + +func TestManager_SelfEcho_SchemeMapping(t *testing.T) { + manager := &Manager{state: &FreedeskState{}} + + manager.ExpectColorSchemeEcho("prefer-light") + assert.True(t, manager.consumeSelfEcho(2)) + + manager.ExpectColorSchemeEcho("default") + assert.True(t, manager.consumeSelfEcho(0)) +} diff --git a/core/internal/server/freedesktop/types.go b/core/internal/server/freedesktop/types.go index 0a9517ab0..0d9d9c5ae 100644 --- a/core/internal/server/freedesktop/types.go +++ b/core/internal/server/freedesktop/types.go @@ -2,11 +2,17 @@ package freedesktop import ( "sync" + "time" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" "github.com/godbus/dbus/v5" ) +type colorSchemeEcho struct { + value uint32 + expires time.Time +} + type AccountsState struct { Available bool `json:"available"` UserPath string `json:"userPath"` @@ -63,4 +69,6 @@ type Manager struct { screensaverCookieCounter uint32 screensaverFreedesktopClaimed bool screensaverGnomeClaimed bool + selfEchoMu sync.Mutex + selfEchoes []colorSchemeEcho } diff --git a/core/internal/server/server.go b/core/internal/server/server.go index c3d9a7631..5d2480f91 100644 --- a/core/internal/server/server.go +++ b/core/internal/server/server.go @@ -17,6 +17,7 @@ import ( "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/AvengeMedia/DankMaterialShell/core/internal/matugen" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness" @@ -190,6 +191,7 @@ func InitializeFreedeskManager() error { } freedesktopManager = manager + matugen.SetColorSchemeEchoHook(manager.ExpectColorSchemeEcho) log.Info("Freedesktop manager initialized") return nil diff --git a/quickshell/Services/PortalService.qml b/quickshell/Services/PortalService.qml index 8011a558b..d302ebedb 100644 --- a/quickshell/Services/PortalService.qml +++ b/quickshell/Services/PortalService.qml @@ -87,22 +87,43 @@ Singleton { } } - function evaluateColorScheme() { + function canSyncColorScheme() { if (typeof SettingsData === "undefined" || !SettingsData.syncModeWithPortal) - return; + return false; if (!settingsPortalAvailable) - return; + return false; if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) - return; - if (typeof Theme === "undefined") - return; - // Defer mid-generation: setLightMode's regen would be dropped, and DMS's own transient color-scheme toggle would be misread. Re-run on worker completion. - if (Theme.workerRunning) + return false; + return typeof Theme !== "undefined"; + } + + // Only follow values stable for the settle window — the GTK4-refresh toggle reverts within ~50ms and following it would loop. + function evaluateColorScheme() { + if (!canSyncColorScheme()) return; const shouldBeLight = systemColorScheme !== 1; - if (Theme.isLightMode === shouldBeLight) + if (Theme.isLightMode === shouldBeLight) { + colorSchemeSettleTimer.stop(); return; - Theme.setLightMode(shouldBeLight, true, false); + } + colorSchemeSettleTimer.restart(); + } + + Timer { + id: colorSchemeSettleTimer + interval: 750 + onTriggered: { + if (!root.canSyncColorScheme()) + return; + const shouldBeLight = root.systemColorScheme !== 1; + if (Theme.isLightMode === shouldBeLight) + return; + if (Theme.workerRunning) { + restart(); + return; + } + Theme.setLightMode(shouldBeLight, true, false); + } } function setLightMode(isLightMode) {