From 0c811e3417a24fd420ca65fc6bccc45efb59ba66 Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 5 Aug 2026 16:55:34 -0400 Subject: [PATCH] gamma: validate config before write and publish state every scheduler wakeup fixes #2967 port 1.5 --- core/internal/server/wayland/manager.go | 51 ++++++++------ core/internal/server/wayland/manager_test.go | 73 ++++++++++++++++++++ quickshell/Services/DisplayService.qml | 4 ++ 3 files changed, 108 insertions(+), 20 deletions(-) diff --git a/core/internal/server/wayland/manager.go b/core/internal/server/wayland/manager.go index a152137f2..f40639065 100644 --- a/core/internal/server/wayland/manager.go +++ b/core/internal/server/wayland/manager.go @@ -759,6 +759,9 @@ func (m *Manager) schedulerLoop() { now := time.Now() m.recalcSchedule(now) + // publish independent of output readiness so night status never + // presents a stale schedule while applies are blocked (#2967) + m.updateStateFromSchedule() waitDur := 24 * time.Hour if enabled { @@ -1104,13 +1107,15 @@ func (m *Manager) SetTemperature(low, high int) error { m.configMutex.Unlock() return nil } - m.config.LowTemp = low - m.config.HighTemp = high - err := m.config.Validate() - m.configMutex.Unlock() - if err != nil { + updated := m.config + updated.LowTemp = low + updated.HighTemp = high + if err := updated.Validate(); err != nil { + m.configMutex.Unlock() return err } + m.config = updated + m.configMutex.Unlock() m.triggerUpdate() return nil } @@ -1122,14 +1127,16 @@ func (m *Manager) SetLocation(lat, lon float64) error { m.configMutex.Unlock() return nil } - m.config.Latitude = &lat - m.config.Longitude = &lon - m.config.UseIPLocation = false - err := m.config.Validate() - m.configMutex.Unlock() - if err != nil { + updated := m.config + updated.Latitude = &lat + updated.Longitude = &lon + updated.UseIPLocation = false + if err := updated.Validate(); err != nil { + m.configMutex.Unlock() return err } + m.config = updated + m.configMutex.Unlock() m.triggerUpdate() return nil } @@ -1164,13 +1171,15 @@ func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error { m.configMutex.Unlock() return nil } - m.config.ManualSunrise = &sunrise - m.config.ManualSunset = &sunset - err := m.config.Validate() - m.configMutex.Unlock() - if err != nil { + updated := m.config + updated.ManualSunrise = &sunrise + updated.ManualSunset = &sunset + if err := updated.Validate(); err != nil { + m.configMutex.Unlock() return err } + m.config = updated + m.configMutex.Unlock() m.triggerUpdate() return nil } @@ -1193,12 +1202,14 @@ func (m *Manager) SetGamma(gamma float64) error { m.configMutex.Unlock() return nil } - m.config.Gamma = gamma - err := m.config.Validate() - m.configMutex.Unlock() - if err != nil { + updated := m.config + updated.Gamma = gamma + if err := updated.Validate(); err != nil { + m.configMutex.Unlock() return err } + m.config = updated + m.configMutex.Unlock() m.triggerUpdate() return nil } diff --git a/core/internal/server/wayland/manager_test.go b/core/internal/server/wayland/manager_test.go index 8c001292f..4d4a4bcc8 100644 --- a/core/internal/server/wayland/manager_test.go +++ b/core/internal/server/wayland/manager_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" + "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control" ) func TestManager_ActorSerializesOutputStateAccess(t *testing.T) { @@ -412,3 +413,75 @@ func TestNewManager_InvalidConfig(t *testing.T) { _, err := NewManager(mockDisplay, config) assert.Error(t, err) } + +func TestSetters_RejectedValuesLeaveConfigUntouched(t *testing.T) { + newManager := func() *Manager { + return &Manager{ + config: DefaultConfig(), + updateTrigger: make(chan struct{}, 1), + } + } + + t.Run("SetTemperature", func(t *testing.T) { + m := newManager() + before := m.config + + err := m.SetTemperature(3200, 2500) + assert.Error(t, err) + assert.Equal(t, before, m.config) + assert.Empty(t, m.updateTrigger) + }) + + t.Run("SetLocation", func(t *testing.T) { + m := newManager() + before := m.config + + err := m.SetLocation(120.0, 10.0) + assert.Error(t, err) + assert.Equal(t, before, m.config) + assert.Empty(t, m.updateTrigger) + }) + + t.Run("SetGamma", func(t *testing.T) { + m := newManager() + before := m.config + + err := m.SetGamma(-1.0) + assert.Error(t, err) + assert.Equal(t, before, m.config) + assert.Empty(t, m.updateTrigger) + }) +} + +func TestSetters_ValidValuesCommitAndTrigger(t *testing.T) { + m := &Manager{ + config: DefaultConfig(), + updateTrigger: make(chan struct{}, 1), + } + + err := m.SetTemperature(3000, 6000) + assert.NoError(t, err) + assert.Equal(t, 3000, m.config.LowTemp) + assert.Equal(t, 6000, m.config.HighTemp) + assert.Len(t, m.updateTrigger, 1) +} + +func TestApplyGamma_SkipsUnchangedTempAndGamma(t *testing.T) { + m := &Manager{config: DefaultConfig()} + m.controlsInitialized = true + + out := &outputState{ + id: 1, + rampSize: 256, + gammaControl: &wlr_gamma_control.ZwlrGammaControlV1{}, + lastTemp: 5000, + lastGamma: m.config.Gamma, + } + m.outputs.Store(out.id, out) + + m.applyGamma(5000) + + assert.False(t, out.failed, "unchanged temp must not reach the compositor write path") + assert.Equal(t, 5000, out.lastTemp) + assert.Equal(t, uint32(256), out.rampSize) +} diff --git a/quickshell/Services/DisplayService.qml b/quickshell/Services/DisplayService.qml index 8ad163acc..d522ffc48 100644 --- a/quickshell/Services/DisplayService.qml +++ b/quickshell/Services/DisplayService.qml @@ -1721,6 +1721,8 @@ Singleton { return "Temperature must be between 2500K and 6000K"; const rounded = Math.round(temp / 500) * 500; + if (rounded > SessionData.nightModeHighTemperature) + return "Night temperature must not exceed the day temperature (" + SessionData.nightModeHighTemperature + "K)"; SessionData.setNightModeTemperature(rounded); if (root.nightModeEnabled) { @@ -1750,6 +1752,8 @@ Singleton { return "Temperature must be between 2500K and 6500K"; const rounded = Math.round(temp / 500) * 500; + if (rounded < SessionData.nightModeTemperature) + return "Day temperature must be at least the night temperature (" + SessionData.nightModeTemperature + "K)"; SessionData.setNightModeHighTemperature(rounded); if (root.nightModeEnabled && SessionData.nightModeAutoEnabled)