From b8cceb7b3a34aff05e3724008c19c30f57132c4c Mon Sep 17 00:00:00 2001 From: bbedward Date: Sat, 29 Aug 2026 16:23:48 -0400 Subject: [PATCH] gamma: configurable transition duration for time-based night mode --- core/internal/errdefs/errdefs.go | 2 ++ core/internal/server/server.go | 4 ++-- core/internal/server/wayland/handlers.go | 8 +++++++- core/internal/server/wayland/manager.go | 16 +++++++++++++--- core/internal/server/wayland/types.go | 3 +++ quickshell/Common/SessionData.qml | 6 ++++++ quickshell/Common/settings/SessionSpec.js | 1 + quickshell/Modules/Settings/GammaControlTab.qml | 14 ++++++++++++++ quickshell/Services/DisplayService.qml | 6 +++++- .../translations/settings_search_index.json | 17 +++++++++++++++++ 10 files changed, 70 insertions(+), 7 deletions(-) diff --git a/core/internal/errdefs/errdefs.go b/core/internal/errdefs/errdefs.go index 636f20f11..6336a9d31 100644 --- a/core/internal/errdefs/errdefs.go +++ b/core/internal/errdefs/errdefs.go @@ -26,6 +26,7 @@ const ( ErrTypeSecretPromptTimeout ErrTypeSecretAgentFailed ErrTypeGeneric + ErrTypeInvalidDuration ) func NewCustomError(errType ErrorType, message string) error { @@ -54,6 +55,7 @@ var ( ErrInvalidGamma = NewCustomError(ErrTypeInvalidGamma, "gamma must be between 0 and 10") ErrInvalidLocation = NewCustomError(ErrTypeInvalidLocation, "invalid latitude/longitude") ErrInvalidManualTimes = NewCustomError(ErrTypeInvalidManualTimes, "both sunrise and sunset must be set or neither") + ErrInvalidDuration = NewCustomError(ErrTypeInvalidDuration, "duration must be between 0 and 6 hours") ErrNoWaylandDisplay = NewCustomError(ErrTypeNoWaylandDisplay, "no wayland display available") ErrNoGammaControl = NewCustomError(ErrTypeNoGammaControl, "compositor does not support gamma control") ErrNotInitialized = NewCustomError(ErrTypeNotInitialized, "manager not initialized") diff --git a/core/internal/server/server.go b/core/internal/server/server.go index 6537ea813..15125bca7 100644 --- a/core/internal/server/server.go +++ b/core/internal/server/server.go @@ -38,7 +38,7 @@ import ( "github.com/AvengeMedia/dankgo/syncmap" ) -const APIVersion = 31 +const APIVersion = 32 var CLIVersion = "dev" @@ -1431,7 +1431,7 @@ func (s *Server) Serve(printDocs bool) error { log.Info(" wayland.gamma.getState - Get current gamma control state") log.Info(" wayland.gamma.setTemperature - Set temperature range (params: low, high)") log.Info(" wayland.gamma.setLocation - Set location (params: latitude, longitude)") - log.Info(" wayland.gamma.setManualTimes - Set manual times (params: sunrise, sunset)") + log.Info(" wayland.gamma.setManualTimes - Set manual times (params: sunrise, sunset, durationMinutes)") log.Info(" wayland.gamma.setGamma - Set gamma value (params: gamma)") log.Info(" wayland.gamma.setEnabled - Enable/disable gamma control (params: enabled)") log.Info(" wayland.gamma.subscribe - Subscribe to gamma state changes (streaming)") diff --git a/core/internal/server/wayland/handlers.go b/core/internal/server/wayland/handlers.go index 4c7deb12b..20386435c 100644 --- a/core/internal/server/wayland/handlers.go +++ b/core/internal/server/wayland/handlers.go @@ -112,7 +112,13 @@ func handleSetManualTimes(conn *models.Conn, req models.Request, manager *Manage return } - if err := manager.SetManualTimes(sunrise, sunset); err != nil { + var duration *time.Duration + if minutes, ok := models.Get[float64](req, "durationMinutes"); ok { + d := time.Duration(minutes) * time.Minute + duration = &d + } + + if err := manager.SetManualTimes(sunrise, sunset, duration); err != nil { models.RespondError(conn, req.ID, err.Error()) return } diff --git a/core/internal/server/wayland/manager.go b/core/internal/server/wayland/manager.go index 166ff7e04..c929450ba 100644 --- a/core/internal/server/wayland/manager.go +++ b/core/internal/server/wayland/manager.go @@ -1157,17 +1157,19 @@ func (m *Manager) SetUseIPLocation(use bool) { m.triggerUpdate() } -func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error { +func (m *Manager) SetManualTimes(sunrise, sunset time.Time, duration *time.Duration) error { m.configMutex.Lock() if m.config.ManualSunrise != nil && m.config.ManualSunset != nil && m.config.ManualSunrise.Hour() == sunrise.Hour() && m.config.ManualSunrise.Minute() == sunrise.Minute() && - m.config.ManualSunset.Hour() == sunset.Hour() && m.config.ManualSunset.Minute() == sunset.Minute() { + m.config.ManualSunset.Hour() == sunset.Hour() && m.config.ManualSunset.Minute() == sunset.Minute() && + durationEqual(m.config.ManualDuration, duration) { m.configMutex.Unlock() return nil } updated := m.config updated.ManualSunrise = &sunrise updated.ManualSunset = &sunset + updated.ManualDuration = duration if err := updated.Validate(); err != nil { m.configMutex.Unlock() return err @@ -1180,16 +1182,24 @@ func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error { func (m *Manager) ClearManualTimes() { m.configMutex.Lock() - if m.config.ManualSunrise == nil && m.config.ManualSunset == nil { + if m.config.ManualSunrise == nil && m.config.ManualSunset == nil && m.config.ManualDuration == nil { m.configMutex.Unlock() return } m.config.ManualSunrise = nil m.config.ManualSunset = nil + m.config.ManualDuration = nil m.configMutex.Unlock() m.triggerUpdate() } +func durationEqual(a, b *time.Duration) bool { + if a == nil || b == nil { + return a == b + } + return *a == *b +} + func (m *Manager) SetGamma(gamma float64) error { m.configMutex.Lock() if m.config.Gamma == gamma { diff --git a/core/internal/server/wayland/types.go b/core/internal/server/wayland/types.go index ef5d8e1d7..8931314dd 100644 --- a/core/internal/server/wayland/types.go +++ b/core/internal/server/wayland/types.go @@ -155,6 +155,9 @@ func (c *Config) Validate() error { if (c.ManualSunrise != nil) != (c.ManualSunset != nil) { return errdefs.ErrInvalidManualTimes } + if c.ManualDuration != nil && (*c.ManualDuration < 0 || *c.ManualDuration > 6*time.Hour) { + return errdefs.ErrInvalidDuration + } return nil } diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index fd351ae09..36390d94a 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -167,6 +167,7 @@ Singleton { property int nightModeStartMinute: 0 property int nightModeEndHour: 6 property int nightModeEndMinute: 0 + property int nightModeTransitionMinutes: 60 property real latitude: 0.0 property real longitude: 0.0 property bool nightModeUseIPLocation: false @@ -1085,6 +1086,11 @@ Singleton { saveSettings(); } + function setNightModeTransitionMinutes(minutes) { + nightModeTransitionMinutes = minutes; + saveSettings(); + } + function setNightModeUseIPLocation(use) { nightModeUseIPLocation = use; saveSettings(); diff --git a/quickshell/Common/settings/SessionSpec.js b/quickshell/Common/settings/SessionSpec.js index d283989bf..fab64c909 100644 --- a/quickshell/Common/settings/SessionSpec.js +++ b/quickshell/Common/settings/SessionSpec.js @@ -36,6 +36,7 @@ var SPEC = { nightModeStartMinute: { def: 0 }, nightModeEndHour: { def: 6 }, nightModeEndMinute: { def: 0 }, + nightModeTransitionMinutes: { def: 60 }, latitude: { def: 0.0 }, longitude: { def: 0.0 }, nightModeUseIPLocation: { def: false }, diff --git a/quickshell/Modules/Settings/GammaControlTab.qml b/quickshell/Modules/Settings/GammaControlTab.qml index f635bf858..f455da9e4 100644 --- a/quickshell/Modules/Settings/GammaControlTab.qml +++ b/quickshell/Modules/Settings/GammaControlTab.qml @@ -318,6 +318,20 @@ Item { } } } + + SettingsSliderRow { + settingKey: "nightModeTransitionMinutes" + tags: ["gamma", "night", "transition", "duration", "fade"] + width: parent.width + text: I18n.tr("Transition Duration") + minimum: 0 + maximum: 180 + step: 5 + unit: "min" + value: SessionData.nightModeTransitionMinutes + visible: DMSService.apiVersion >= 32 + onSliderValueChanged: newValue => SessionData.setNightModeTransitionMinutes(newValue) + } } SettingsLocationSection { diff --git a/quickshell/Services/DisplayService.qml b/quickshell/Services/DisplayService.qml index 8e384addd..35c61c72d 100644 --- a/quickshell/Services/DisplayService.qml +++ b/quickshell/Services/DisplayService.qml @@ -1196,7 +1196,8 @@ Singleton { DMSService.sendRequest("wayland.gamma.setManualTimes", { "sunrise": sunrise, - "sunset": sunset + "sunset": sunset, + "durationMinutes": SessionData.nightModeTransitionMinutes }, response => { if (response.error) { log.error("Failed to set manual times:", response.error); @@ -1534,6 +1535,9 @@ Singleton { function onNightModeEndMinuteChanged() { evaluateNightMode(); } + function onNightModeTransitionMinutesChanged() { + evaluateNightMode(); + } function onNightModeTemperatureChanged() { evaluateNightMode(); } diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index e538e5f45..67aaa8aa1 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -8969,6 +8969,23 @@ ], "description": "Color temperature for night mode" }, + { + "section": "nightModeTransitionMinutes", + "label": "Transition Duration", + "tabIndex": 25, + "category": "Displays", + "keywords": [ + "displays", + "duration", + "fade", + "gamma", + "monitor", + "night", + "resolution", + "screen", + "transition" + ] + }, { "section": "_tab_26", "label": "Widgets",