diff options
| author | Nippy <nippy@rp1.hu> | 2026-05-09 13:33:09 +0200 |
|---|---|---|
| committer | Nippy <nippy@rp1.hu> | 2026-05-09 13:33:09 +0200 |
| commit | a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5 (patch) | |
| tree | 1a8769217f84bfe9d6216fafaadaf8cdd876d457 /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland | |
| parent | 34b9c34f982b2596cfa9e368a2d7f74e9a17477c (diff) | |
| download | RaveOS-PKGBUILD-a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5.tar.gz RaveOS-PKGBUILD-a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5.zip | |
raveos update
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland')
10 files changed, 0 insertions, 3244 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma.go deleted file mode 100644 index 1fff03f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma.go +++ /dev/null @@ -1,167 +0,0 @@ -package wayland - -import ( - "math" -) - -type GammaRamp struct { - Red []uint16 - Green []uint16 - Blue []uint16 -} - -type rgb struct { - r, g, b float64 -} - -type xyz struct { - x, y, z float64 -} - -func illuminantD(temp int) (float64, float64, bool) { - var x float64 - switch { - case temp >= 2500 && temp <= 7000: - t := float64(temp) - x = 0.244063 + 0.09911e3/t + 2.9678e6/(t*t) - 4.6070e9/(t*t*t) - case temp > 7000 && temp <= 25000: - t := float64(temp) - x = 0.237040 + 0.24748e3/t + 1.9018e6/(t*t) - 2.0064e9/(t*t*t) - default: - return 0, 0, false - } - y := -3*(x*x) + 2.870*x - 0.275 - return x, y, true -} - -func planckianLocus(temp int) (float64, float64, bool) { - var x, y float64 - switch { - case temp >= 1667 && temp <= 4000: - t := float64(temp) - x = -0.2661239e9/(t*t*t) - 0.2343589e6/(t*t) + 0.8776956e3/t + 0.179910 - if temp <= 2222 { - y = -1.1064814*(x*x*x) - 1.34811020*(x*x) + 2.18555832*x - 0.20219683 - } else { - y = -0.9549476*(x*x*x) - 1.37418593*(x*x) + 2.09137015*x - 0.16748867 - } - case temp > 4000 && temp < 25000: - t := float64(temp) - x = -3.0258469e9/(t*t*t) + 2.1070379e6/(t*t) + 0.2226347e3/t + 0.240390 - y = 3.0817580*(x*x*x) - 5.87338670*(x*x) + 3.75112997*x - 0.37001483 - default: - return 0, 0, false - } - return x, y, true -} - -func srgbGamma(value, gamma float64) float64 { - if value <= 0.0031308 { - return 12.92 * value - } - return math.Pow(1.055*value, 1.0/gamma) - 0.055 -} - -func clamp01(v float64) float64 { - switch { - case v > 1.0: - return 1.0 - case v < 0.0: - return 0.0 - default: - return v - } -} - -func xyzToSRGB(c xyz) rgb { - return rgb{ - r: srgbGamma(clamp01(3.2404542*c.x-1.5371385*c.y-0.4985314*c.z), 2.2), - g: srgbGamma(clamp01(-0.9692660*c.x+1.8760108*c.y+0.0415560*c.z), 2.2), - b: srgbGamma(clamp01(0.0556434*c.x-0.2040259*c.y+1.0572252*c.z), 2.2), - } -} - -func normalizeRGB(c *rgb) { - maxw := math.Max(c.r, math.Max(c.g, c.b)) - if maxw > 0 { - c.r /= maxw - c.g /= maxw - c.b /= maxw - } -} - -func calcWhitepoint(temp int) rgb { - if temp == 6500 { - return rgb{r: 1.0, g: 1.0, b: 1.0} - } - - var wp xyz - - switch { - case temp >= 25000: - x, y, _ := illuminantD(25000) - wp.x = x - wp.y = y - case temp >= 4000: - x, y, _ := illuminantD(temp) - wp.x = x - wp.y = y - case temp >= 2500: - x1, y1, _ := illuminantD(temp) - x2, y2, _ := planckianLocus(temp) - factor := float64(4000-temp) / 1500.0 - sineFactor := (math.Cos(math.Pi*factor) + 1.0) / 2.0 - wp.x = x1*sineFactor + x2*(1.0-sineFactor) - wp.y = y1*sineFactor + y2*(1.0-sineFactor) - default: - t := temp - if t < 1667 { - t = 1667 - } - x, y, _ := planckianLocus(t) - wp.x = x - wp.y = y - } - - wp.z = 1.0 - wp.x - wp.y - - wpRGB := xyzToSRGB(wp) - normalizeRGB(&wpRGB) - return wpRGB -} - -func GenerateGammaRamp(size uint32, temp int, gamma float64) GammaRamp { - ramp := GammaRamp{ - Red: make([]uint16, size), - Green: make([]uint16, size), - Blue: make([]uint16, size), - } - - wp := calcWhitepoint(temp) - - for i := uint32(0); i < size; i++ { - val := float64(i) / float64(size-1) - ramp.Red[i] = uint16(clamp01(math.Pow(val*wp.r, 1.0/gamma)) * 65535.0) - ramp.Green[i] = uint16(clamp01(math.Pow(val*wp.g, 1.0/gamma)) * 65535.0) - ramp.Blue[i] = uint16(clamp01(math.Pow(val*wp.b, 1.0/gamma)) * 65535.0) - } - - return ramp -} - -func GenerateIdentityRamp(size uint32) GammaRamp { - ramp := GammaRamp{ - Red: make([]uint16, size), - Green: make([]uint16, size), - Blue: make([]uint16, size), - } - - for i := uint32(0); i < size; i++ { - val := uint16((float64(i) / float64(size-1)) * 65535.0) - ramp.Red[i] = val - ramp.Green[i] = val - ramp.Blue[i] = val - } - - return ramp -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma_test.go deleted file mode 100644 index 0bf8e71..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/gamma_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package wayland - -import ( - "testing" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -func TestGenerateGammaRamp(t *testing.T) { - tests := []struct { - name string - size uint32 - temp int - gamma float64 - }{ - {"small_warm", 16, 6500, 1.0}, - {"small_cool", 16, 4000, 1.0}, - {"large_warm", 256, 6500, 1.0}, - {"large_cool", 256, 4000, 1.0}, - {"custom_gamma", 64, 5500, 1.2}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ramp := GenerateGammaRamp(tt.size, tt.temp, tt.gamma) - - if len(ramp.Red) != int(tt.size) { - t.Errorf("expected %d red values, got %d", tt.size, len(ramp.Red)) - } - if len(ramp.Green) != int(tt.size) { - t.Errorf("expected %d green values, got %d", tt.size, len(ramp.Green)) - } - if len(ramp.Blue) != int(tt.size) { - t.Errorf("expected %d blue values, got %d", tt.size, len(ramp.Blue)) - } - - if ramp.Red[0] != 0 || ramp.Green[0] != 0 || ramp.Blue[0] != 0 { - t.Errorf("first values should be 0, got R:%d G:%d B:%d", - ramp.Red[0], ramp.Green[0], ramp.Blue[0]) - } - - lastIdx := tt.size - 1 - if ramp.Red[lastIdx] == 0 || ramp.Green[lastIdx] == 0 || ramp.Blue[lastIdx] == 0 { - t.Errorf("last values should be non-zero, got R:%d G:%d B:%d", - ramp.Red[lastIdx], ramp.Green[lastIdx], ramp.Blue[lastIdx]) - } - - for i := uint32(1); i < tt.size; i++ { - if ramp.Red[i] < ramp.Red[i-1] { - t.Errorf("red ramp not monotonic at index %d", i) - } - } - }) - } -} - -func TestCalcWhitepoint(t *testing.T) { - tests := []struct { - name string - temp int - }{ - {"very_warm", 6500}, - {"neutral", 5500}, - {"cool", 4000}, - {"very_cool", 3000}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - wp := calcWhitepoint(tt.temp) - - if wp.r < 0 || wp.r > 1 { - t.Errorf("red out of range: %f", wp.r) - } - if wp.g < 0 || wp.g > 1 { - t.Errorf("green out of range: %f", wp.g) - } - if wp.b < 0 || wp.b > 1 { - t.Errorf("blue out of range: %f", wp.b) - } - }) - } -} - -func TestWhitepointProgression(t *testing.T) { - temps := []int{3000, 4000, 5000, 6000, 6500} - - var prevBlue float64 - for i, temp := range temps { - wp := calcWhitepoint(temp) - if i > 0 && wp.b < prevBlue { - t.Errorf("blue should increase with temperature, %d->%d: %f->%f", - temps[i-1], temp, prevBlue, wp.b) - } - prevBlue = wp.b - } -} - -func TestClamp(t *testing.T) { - tests := []struct { - val float64 - min float64 - max float64 - expected float64 - }{ - {5, 0, 10, 5}, - {-5, 0, 10, 0}, - {15, 0, 10, 10}, - {0, 0, 10, 0}, - {10, 0, 10, 10}, - } - - for _, tt := range tests { - result := utils.Clamp(tt.val, tt.min, tt.max) - if result != tt.expected { - t.Errorf("clamp(%f, %f, %f) = %f, want %f", - tt.val, tt.min, tt.max, result, tt.expected) - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/geolocation.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/geolocation.go deleted file mode 100644 index ba99b5f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/geolocation.go +++ /dev/null @@ -1,50 +0,0 @@ -package wayland - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -type ipAPIResponse struct { - Lat float64 `json:"lat"` - Lon float64 `json:"lon"` - City string `json:"city"` -} - -func FetchIPLocation() (*float64, *float64, error) { - client := &http.Client{ - Timeout: 10 * time.Second, - } - - resp, err := client.Get("http://ip-api.com/json/") - if err != nil { - return nil, nil, fmt.Errorf("failed to fetch IP location: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, nil, fmt.Errorf("ip-api.com returned status %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response: %w", err) - } - - var data ipAPIResponse - if err := json.Unmarshal(body, &data); err != nil { - return nil, nil, fmt.Errorf("failed to parse response: %w", err) - } - - if data.Lat == 0 && data.Lon == 0 { - return nil, nil, fmt.Errorf("missing location data in response") - } - - log.Infof("Fetched IP-based location: %s (%.4f, %.4f)", data.City, data.Lat, data.Lon) - return &data.Lat, &data.Lon, nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/handlers.go deleted file mode 100644 index 76b08b5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/handlers.go +++ /dev/null @@ -1,182 +0,0 @@ -package wayland - -import ( - "encoding/json" - "fmt" - "net" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - if manager == nil { - models.RespondError(conn, req.ID, "wayland manager not initialized") - return - } - - switch req.Method { - case "wayland.gamma.getState": - handleGetState(conn, req, manager) - case "wayland.gamma.setTemperature": - handleSetTemperature(conn, req, manager) - case "wayland.gamma.setLocation": - handleSetLocation(conn, req, manager) - case "wayland.gamma.setManualTimes": - handleSetManualTimes(conn, req, manager) - case "wayland.gamma.setUseIPLocation": - handleSetUseIPLocation(conn, req, manager) - case "wayland.gamma.setGamma": - handleSetGamma(conn, req, manager) - case "wayland.gamma.setEnabled": - handleSetEnabled(conn, req, manager) - case "wayland.gamma.subscribe": - handleSubscribe(conn, req, manager) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleGetState(conn net.Conn, req models.Request, manager *Manager) { - models.Respond(conn, req.ID, manager.GetState()) -} - -func handleSetTemperature(conn net.Conn, req models.Request, manager *Manager) { - var lowTemp, highTemp int - - if temp, ok := models.Get[float64](req, "temp"); ok { - lowTemp = int(temp) - highTemp = int(temp) - } else { - low, err := params.Float(req.Params, "low") - if err != nil { - models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')") - return - } - high, err := params.Float(req.Params, "high") - if err != nil { - models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')") - return - } - lowTemp = int(low) - highTemp = int(high) - } - - if err := manager.SetTemperature(lowTemp, highTemp); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "temperature set"}) -} - -func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) { - lat, err := params.Float(req.Params, "latitude") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - lon, err := params.Float(req.Params, "longitude") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetLocation(lat, lon); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"}) -} - -func handleSetManualTimes(conn net.Conn, req models.Request, manager *Manager) { - sunriseStr, sunriseOK := models.Get[string](req, "sunrise") - sunsetStr, sunsetOK := models.Get[string](req, "sunset") - - if !sunriseOK || !sunsetOK || sunriseStr == "" || sunsetStr == "" { - manager.ClearManualTimes() - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times cleared"}) - return - } - - sunrise, err := time.Parse("15:04", sunriseStr) - if err != nil { - models.RespondError(conn, req.ID, "invalid sunrise format (use HH:MM)") - return - } - - sunset, err := time.Parse("15:04", sunsetStr) - if err != nil { - models.RespondError(conn, req.ID, "invalid sunset format (use HH:MM)") - return - } - - if err := manager.SetManualTimes(sunrise, sunset); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "manual times set"}) -} - -func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) { - use, err := params.Bool(req.Params, "use") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - manager.SetUseIPLocation(use) - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "IP location preference set"}) -} - -func handleSetGamma(conn net.Conn, req models.Request, manager *Manager) { - gamma, err := params.Float(req.Params, "gamma") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetGamma(gamma); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "gamma set"}) -} - -func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) { - enabled, err := params.Bool(req.Params, "enabled") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - manager.SetEnabled(enabled) - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "enabled state set"}) -} - -func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) { - clientID := fmt.Sprintf("client-%p", conn) - stateChan := manager.Subscribe(clientID) - defer manager.Unsubscribe(clientID) - - initialState := manager.GetState() - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - ID: req.ID, - Result: &initialState, - }); err != nil { - return - } - - for state := range stateChan { - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - Result: &state, - }); err != nil { - return - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager.go deleted file mode 100644 index 3540e3d..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager.go +++ /dev/null @@ -1,1256 +0,0 @@ -package wayland - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" - "os" - "slices" - "syscall" - "time" - - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" - "github.com/godbus/dbus/v5" - "golang.org/x/sys/unix" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control" -) - -const animKelvinStep = 25 - -func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) { - if err := config.Validate(); err != nil { - return nil, err - } - - if config.ElevationTwilight == 0 { - config.ElevationTwilight = -6.0 - } - if config.ElevationDaylight == 0 { - config.ElevationDaylight = 3.0 - } - - m := &Manager{ - config: config, - display: display, - ctx: display.Context(), - cmdq: make(chan cmd, 128), - stopChan: make(chan struct{}), - updateTrigger: make(chan struct{}, 1), - dirty: make(chan struct{}, 1), - dbusSignal: make(chan *dbus.Signal, 16), - } - - if err := m.setupRegistry(); err != nil { - return nil, err - } - - if err := m.setupDBusMonitor(); err != nil { - log.Warnf("Failed to setup D-Bus monitoring: %v", err) - } - - m.alive = true - m.recalcSchedule(time.Now()) - m.updateStateFromSchedule() - - m.notifierWg.Add(1) - go m.notifier() - - m.wg.Add(1) - go m.schedulerLoop() - - if m.dbusConn != nil { - m.wg.Add(1) - go m.dbusMonitor() - } - - m.wg.Add(1) - go m.waylandActor() - - if config.Enabled { - m.post(func() { - log.Info("Gamma control enabled at startup") - gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) - m.availOutputsMu.RLock() - outs := slices.Clone(m.availableOutputs) - m.availOutputsMu.RUnlock() - if err := m.setupOutputControls(outs, gammaMgr); err != nil { - log.Errorf("Failed to initialize gamma controls: %v", err) - return - } - m.controlsInitialized = true - }) - } - - return m, nil -} - -func (m *Manager) post(fn func()) { - select { - case m.cmdq <- cmd{fn: fn}: - default: - log.Warn("Actor command queue full") - } -} - -func (m *Manager) waylandActor() { - defer m.wg.Done() - for { - select { - case <-m.stopChan: - return - case c := <-m.cmdq: - c.fn() - } - } -} - -func (m *Manager) anyOutputReady() bool { - anyReady := false - m.outputs.Range(func(_ uint32, out *outputState) bool { - if out.rampSize > 0 && !out.failed { - anyReady = true - return false // stop iteration - } - return true - }) - return anyReady -} - -func (m *Manager) setupDBusMonitor() error { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return fmt.Errorf("system bus: %w", err) - } - - matchRule := "type='signal',interface='org.freedesktop.login1.Manager',member='PrepareForSleep',path='/org/freedesktop/login1'" - if err := conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule).Err; err != nil { - conn.Close() - return fmt.Errorf("add match: %w", err) - } - - conn.Signal(m.dbusSignal) - m.dbusConn = conn - return nil -} - -func (m *Manager) setupRegistry() error { - registry, err := m.display.GetRegistry() - if err != nil { - return fmt.Errorf("get registry: %w", err) - } - m.registry = registry - - outputs := make([]*wlclient.Output, 0) - outputNames := make(map[uint32]string) - var gammaMgr *wlr_gamma_control.ZwlrGammaControlManagerV1 - - registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { - switch e.Interface { - case wlr_gamma_control.ZwlrGammaControlManagerV1InterfaceName: - manager := wlr_gamma_control.NewZwlrGammaControlManagerV1(m.ctx) - version := e.Version - if version > 1 { - version = 1 - } - if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil { - gammaMgr = manager - } - case "wl_output": - output := wlclient.NewOutput(m.ctx) - version := e.Version - if version > 4 { - version = 4 - } - if err := registry.Bind(e.Name, e.Interface, version, output); err != nil { - return - } - outputID := output.ID() - output.SetNameHandler(func(ev wlclient.OutputNameEvent) { - outputNames[outputID] = ev.Name - }) - if gammaMgr != nil { - outputs = append(outputs, output) - m.addAvailableOutput(output) - } - m.outputRegNames.Store(outputID, e.Name) - - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - - if enabled && m.controlsInitialized { - m.post(func() { - if err := m.addOutputControl(output); err != nil { - log.Warnf("Failed to add output control: %v", err) - } - }) - } - } - }) - - registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) { - m.post(func() { - var foundID uint32 - var foundOut *outputState - m.outputs.Range(func(id uint32, out *outputState) bool { - if out.registryName == e.Name { - foundID = id - foundOut = out - return false - } - return true - }) - if foundOut == nil { - return - } - if foundOut.gammaControl != nil { - foundOut.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy() - foundOut.gammaControl = nil - } - m.removeAvailableOutput(foundOut.output) - if foundOut.output != nil && !foundOut.output.IsZombie() { - _ = foundOut.output.Release() - } - m.outputs.Delete(foundID) - - hasOutputs := false - m.outputs.Range(func(_ uint32, _ *outputState) bool { - hasOutputs = true - return false - }) - if !hasOutputs { - m.controlsInitialized = false - } - }) - }) - - if err := m.display.Roundtrip(); err != nil { - return fmt.Errorf("roundtrip 1: %w", err) - } - if err := m.display.Roundtrip(); err != nil { - return fmt.Errorf("roundtrip 2: %w", err) - } - - if gammaMgr == nil { - return errdefs.ErrNoGammaControl - } - if len(outputs) == 0 { - return fmt.Errorf("no outputs") - } - - physicalOutputs := make([]*wlclient.Output, 0, len(outputs)) - for _, output := range outputs { - name := outputNames[output.ID()] - if len(name) >= 9 && name[:9] == "HEADLESS-" { - continue - } - physicalOutputs = append(physicalOutputs, output) - } - - m.gammaControl = gammaMgr - m.availableOutputs = physicalOutputs - return nil -} - -func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_gamma_control.ZwlrGammaControlManagerV1) error { - for _, output := range outputs { - control, err := manager.GetGammaControl(output) - if err != nil { - continue - } - outputID := output.ID() - registryName, _ := m.outputRegNames.Load(outputID) - outState := &outputState{ - id: outputID, - registryName: registryName, - output: output, - gammaControl: control, - } - m.setupControlHandlers(outState, control) - m.outputs.Store(outputID, outState) - } - return nil -} - -func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_control.ZwlrGammaControlV1) { - outputID := state.id - - control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { - size := e.Size - m.post(func() { - if out, ok := m.outputs.Load(outputID); ok { - out.rampSize = size - out.failed = false - out.retryCount = 0 - } - m.lastAppliedTemp = 0 - m.applyCurrentTemp("gamma_size") - }) - }) - - control.SetFailedHandler(func(_ wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { - m.post(func() { - out, ok := m.outputs.Load(outputID) - if !ok { - return - } - if ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok && ctrl != nil && !ctrl.IsZombie() { - ctrl.Destroy() - } - out.gammaControl = nil - out.failed = true - out.rampSize = 0 - out.retryCount++ - out.lastFailTime = time.Now() - - if !m.outputStillValid(out) { - return - } - - backoff := time.Duration(300<<uint(min(out.retryCount-1, 4))) * time.Millisecond - time.AfterFunc(backoff, func() { - m.post(func() { - if !m.outputStillValid(out) { - return - } - if _, stillTracked := m.outputs.Load(outputID); !stillTracked { - return - } - m.recreateOutputControl(out) - }) - }) - }) - }) -} - -func (m *Manager) addAvailableOutput(o *wlclient.Output) { - if o == nil { - return - } - m.availOutputsMu.Lock() - defer m.availOutputsMu.Unlock() - if slices.Contains(m.availableOutputs, o) { - return - } - m.availableOutputs = append(m.availableOutputs, o) -} - -func (m *Manager) removeAvailableOutput(o *wlclient.Output) { - if o == nil { - return - } - m.availOutputsMu.Lock() - defer m.availOutputsMu.Unlock() - m.availableOutputs = slices.DeleteFunc(m.availableOutputs, func(existing *wlclient.Output) bool { - return existing == o - }) -} - -func (m *Manager) outputStillValid(out *outputState) bool { - switch { - case out == nil: - return false - case out.output == nil: - return false - case out.output.IsZombie(): - return false - } - m.availOutputsMu.RLock() - defer m.availOutputsMu.RUnlock() - return slices.Contains(m.availableOutputs, out.output) -} - -func isConnectionDeadErr(err error) bool { - switch { - case err == nil: - return false - case errors.Is(err, syscall.EPIPE): - return true - case errors.Is(err, syscall.ECONNRESET): - return true - case errors.Is(err, syscall.EBADF): - return true - case errors.Is(err, io.EOF): - return true - } - return false -} - -func (m *Manager) addOutputControl(output *wlclient.Output) error { - switch { - case m.connectionDead.Load(): - return nil - case output == nil || output.IsZombie(): - return nil - } - - outputID := output.ID() - gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) - - control, err := gammaMgr.GetGammaControl(output) - if err != nil { - if isConnectionDeadErr(err) { - m.markConnectionDead(err) - } - return err - } - - registryName, _ := m.outputRegNames.Load(outputID) - outState := &outputState{ - id: outputID, - registryName: registryName, - output: output, - gammaControl: control, - } - m.setupControlHandlers(outState, control) - m.outputs.Store(outputID, outState) - return nil -} - -func (m *Manager) recreateOutputControl(out *outputState) error { - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - - switch { - case m.connectionDead.Load(): - return nil - case !enabled || !m.controlsInitialized: - return nil - case out.isVirtual: - return nil - case out.retryCount >= 10: - return nil - case !m.outputStillValid(out): - return nil - } - if _, ok := m.outputs.Load(out.id); !ok { - return nil - } - - gammaMgr, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) - if !ok { - return fmt.Errorf("no gamma manager") - } - - if existing, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok && existing != nil && !existing.IsZombie() { - existing.Destroy() - out.gammaControl = nil - } - - control, err := gammaMgr.GetGammaControl(out.output) - if err != nil { - if isConnectionDeadErr(err) { - m.markConnectionDead(err) - } - return err - } - - m.setupControlHandlers(out, control) - out.gammaControl = control - out.failed = false - return nil -} - -func (m *Manager) markConnectionDead(err error) { - if m.connectionDead.Swap(true) { - return - } - log.Errorf("gamma: wayland connection appears dead (%v); pausing gamma operations", err) -} - -func (m *Manager) recalcSchedule(now time.Time) { - m.configMutex.RLock() - config := m.config - m.configMutex.RUnlock() - - m.scheduleMutex.Lock() - defer m.scheduleMutex.Unlock() - - dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - alreadyValid := !m.schedule.times.Sunrise.IsZero() - if m.schedule.calcDay.Equal(dayStart) && alreadyValid { - return - } - - var times SunTimes - var cond SunCondition - - if config.ManualSunrise != nil && config.ManualSunset != nil { - dur := time.Hour - if config.ManualDuration != nil { - dur = *config.ManualDuration - } - sunrise := time.Date(now.Year(), now.Month(), now.Day(), - config.ManualSunrise.Hour(), config.ManualSunrise.Minute(), config.ManualSunrise.Second(), 0, now.Location()) - sunset := time.Date(now.Year(), now.Month(), now.Day(), - config.ManualSunset.Hour(), config.ManualSunset.Minute(), config.ManualSunset.Second(), 0, now.Location()) - times = SunTimes{ - Dawn: sunrise.Add(-dur), - Sunrise: sunrise, - Sunset: sunset, - Night: sunset.Add(dur), - } - cond = SunNormal - } else { - lat, lon := m.getLocation() - if lat == nil || lon == nil { - m.gammaState = StateStatic - return - } - times, cond = CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight) - } - - m.schedule.calcDay = dayStart - m.schedule.times = times - m.schedule.condition = cond - - switch cond { - case SunNormal: - m.gammaState = StateNormal - tempDiff := config.HighTemp - config.LowTemp - if tempDiff > 0 { - dawnDur := times.Sunrise.Sub(times.Dawn) - nightDur := times.Night.Sub(times.Sunset) - m.schedule.dawnStepTime = time.Duration(max(1, int(dawnDur.Seconds())*animKelvinStep/tempDiff)) * time.Second - m.schedule.nightStepTime = time.Duration(max(1, int(nightDur.Seconds())*animKelvinStep/tempDiff)) * time.Second - } - case SunMidnightSun: - m.gammaState = StateStatic - case SunPolarNight: - m.gammaState = StateStatic - } -} - -func (m *Manager) SetGeoClient(client geolocation.Client) { - m.geoClient = client -} - -func (m *Manager) getLocation() (*float64, *float64) { - m.configMutex.RLock() - config := m.config - m.configMutex.RUnlock() - - if config.Latitude != nil && config.Longitude != nil { - return config.Latitude, config.Longitude - } - if !config.UseIPLocation { - return nil, nil - } - if m.geoClient == nil { - return nil, nil - } - - m.locationMutex.RLock() - if m.cachedIPLat != nil && m.cachedIPLon != nil { - lat, lon := m.cachedIPLat, m.cachedIPLon - m.locationMutex.RUnlock() - return lat, lon - } - m.locationMutex.RUnlock() - - location, err := m.geoClient.GetLocation() - if err != nil { - return nil, nil - } - - m.locationMutex.Lock() - m.cachedIPLat = &location.Latitude - m.cachedIPLon = &location.Longitude - m.locationMutex.Unlock() - return m.cachedIPLat, m.cachedIPLon -} - -func (m *Manager) hasValidSchedule() bool { - m.scheduleMutex.RLock() - defer m.scheduleMutex.RUnlock() - return !m.schedule.times.Sunrise.IsZero() -} - -func (m *Manager) getSunPosition(now time.Time) float64 { - m.scheduleMutex.RLock() - sched := m.schedule - state := m.gammaState - m.scheduleMutex.RUnlock() - - if sched.times.Sunrise.IsZero() { - return 1.0 - } - - switch state { - case StateStatic: - if sched.condition == SunMidnightSun { - return 1.0 - } - return 0.0 - case StateNormal: - return m.getSunPositionNormal(now, sched.times) - } - return 1.0 -} - -func (m *Manager) getSunPositionNormal(now time.Time, times SunTimes) float64 { - if now.Before(times.Dawn) { - return 0.0 - } - if now.Before(times.Sunrise) { - return interpolate(now, times.Dawn, times.Sunrise) - } - if now.Before(times.Sunset) { - return 1.0 - } - if now.Before(times.Night) { - return interpolate(now, times.Night, times.Sunset) - } - return 0.0 -} - -func interpolate(now time.Time, start, stop time.Time) float64 { - if start.Equal(stop) { - return 1.0 - } - pos := float64(now.Sub(start)) / float64(stop.Sub(start)) - switch { - case pos > 1.0: - return 1.0 - case pos < 0.0: - return 0.0 - default: - return pos - } -} - -func (m *Manager) getTempFromPosition(pos float64) int { - m.configMutex.RLock() - low, high := m.config.LowTemp, m.config.HighTemp - m.configMutex.RUnlock() - return low + int(float64(high-low)*pos) -} - -func (m *Manager) getNextDeadline(now time.Time) time.Time { - m.scheduleMutex.RLock() - sched := m.schedule - state := m.gammaState - m.scheduleMutex.RUnlock() - - switch state { - case StateStatic: - return m.tomorrow(now) - case StateNormal: - return m.getDeadlineNormal(now, sched) - default: - return m.tomorrow(now) - } -} - -func (m *Manager) getDeadlineNormal(now time.Time, sched sunSchedule) time.Time { - times := sched.times - switch { - case now.Before(times.Dawn): - return times.Dawn - case now.Before(times.Sunrise): - return now.Add(sched.dawnStepTime) - case now.Before(times.Sunset): - return times.Sunset - case now.Before(times.Night): - return now.Add(sched.nightStepTime) - default: - return m.tomorrowDawn(now) - } -} - -func (m *Manager) tomorrowDawn(now time.Time) time.Time { - tomorrow := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location()) - - m.configMutex.RLock() - config := m.config - m.configMutex.RUnlock() - - if config.ManualSunrise != nil { - dur := time.Hour - if config.ManualDuration != nil { - dur = *config.ManualDuration - } - return time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), - config.ManualSunrise.Hour(), config.ManualSunrise.Minute(), config.ManualSunrise.Second(), 0, tomorrow.Location()).Add(-dur) - } - - lat, lon := m.getLocation() - if lat == nil || lon == nil { - return tomorrow - } - - times, cond := CalculateSunTimesWithTwilight(*lat, *lon, tomorrow, config.ElevationTwilight, config.ElevationDaylight) - if cond != SunNormal { - return tomorrow - } - return times.Dawn -} - -func (m *Manager) tomorrow(now time.Time) time.Time { - return time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location()) -} - -func (m *Manager) schedulerLoop() { - defer m.wg.Done() - - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - - if enabled { - m.post(func() { m.applyCurrentTemp("startup") }) - } - - var timer *time.Timer - for { - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - - now := time.Now() - m.recalcSchedule(now) - - var waitDur time.Duration - if enabled { - deadline := m.getNextDeadline(now) - waitDur = time.Until(deadline) - if waitDur < time.Second { - waitDur = time.Second - } - } else { - waitDur = 24 * time.Hour - } - - if timer != nil { - timer.Stop() - } - timer = time.NewTimer(waitDur) - - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.updateTrigger: - timer.Stop() - m.scheduleMutex.Lock() - m.schedule.calcDay = time.Time{} - m.scheduleMutex.Unlock() - m.recalcSchedule(time.Now()) - m.updateStateFromSchedule() - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - if enabled { - m.post(func() { m.applyCurrentTemp("updateTrigger") }) - } - case <-timer.C: - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - if enabled { - m.post(func() { m.applyCurrentTemp("timer") }) - } - } - } -} - -func (m *Manager) applyCurrentTemp(_ string) { - if !m.controlsInitialized || !m.anyOutputReady() { - return - } - - // Ensure schedule is up-to-date (handles display wake after overnight sleep) - m.recalcSchedule(time.Now()) - - m.configMutex.RLock() - low, high := m.config.LowTemp, m.config.HighTemp - m.configMutex.RUnlock() - - if low == high { - m.applyGamma(low) - m.updateStateFromSchedule() - return - } - - if !m.hasValidSchedule() { - m.updateStateFromSchedule() - return - } - - now := time.Now() - pos := m.getSunPosition(now) - temp := m.getTempFromPosition(pos) - - m.applyGamma(temp) - m.updateStateFromSchedule() -} - -func (m *Manager) applyGamma(temp int) { - m.configMutex.RLock() - gamma := m.config.Gamma - m.configMutex.RUnlock() - - switch { - case m.connectionDead.Load(): - return - case !m.controlsInitialized: - return - case m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma: - return - } - - var outs []*outputState - m.outputs.Range(func(_ uint32, out *outputState) bool { - outs = append(outs, out) - return true - }) - if len(outs) == 0 { - return - } - - type job struct { - out *outputState - data []byte - } - var jobs []job - - for _, out := range outs { - switch { - case out.failed: - continue - case out.rampSize == 0: - continue - case out.gammaControl == nil: - continue - case !m.outputStillValid(out): - continue - } - ramp := GenerateGammaRamp(out.rampSize, temp, gamma) - buf := bytes.NewBuffer(make([]byte, 0, int(out.rampSize)*6)) - for _, v := range ramp.Red { - binary.Write(buf, binary.LittleEndian, v) - } - for _, v := range ramp.Green { - binary.Write(buf, binary.LittleEndian, v) - } - for _, v := range ramp.Blue { - binary.Write(buf, binary.LittleEndian, v) - } - jobs = append(jobs, job{out: out, data: buf.Bytes()}) - } - - for _, j := range jobs { - err := m.setGammaBytes(j.out, j.data) - if err == nil { - continue - } - log.Warnf("gamma: failed to set output %d: %v", j.out.id, err) - j.out.failed = true - j.out.rampSize = 0 - if isConnectionDeadErr(err) { - m.markConnectionDead(err) - return - } - } - - m.lastAppliedTemp = temp - m.lastAppliedGamma = gamma -} - -func (m *Manager) setGammaBytes(out *outputState, data []byte) error { - if out.gammaControl == nil { - return fmt.Errorf("no gamma control") - } - ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) - if !ok || ctrl == nil || ctrl.IsZombie() { - return fmt.Errorf("gamma control invalid") - } - - fd, err := MemfdCreate("gamma-ramp", 0) - if err != nil { - return err - } - defer syscall.Close(fd) - - if err := syscall.Ftruncate(fd, int64(len(data))); err != nil { - return err - } - - dupFd, err := syscall.Dup(fd) - if err != nil { - return err - } - f := os.NewFile(uintptr(dupFd), "gamma") - defer f.Close() - - if _, err := f.Write(data); err != nil { - return err - } - syscall.Seek(fd, 0, 0) - - return ctrl.SetGamma(fd) -} - -func (m *Manager) updateStateFromSchedule() { - now := time.Now() - - m.configMutex.RLock() - config := m.config - m.configMutex.RUnlock() - - m.scheduleMutex.RLock() - times := m.schedule.times - m.scheduleMutex.RUnlock() - - var pos float64 - var temp int - var isDay bool - var deadline time.Time - - if times.Sunrise.IsZero() { - pos = 1.0 - temp = config.HighTemp - isDay = true - deadline = m.tomorrow(now) - } else { - pos = m.getSunPosition(now) - temp = m.getTempFromPosition(pos) - deadline = m.getNextDeadline(now) - isDay = now.After(times.Sunrise) && now.Before(times.Sunset) - } - - newState := State{ - Config: config, - CurrentTemp: temp, - NextTransition: deadline, - SunriseTime: times.Sunrise, - SunsetTime: times.Sunset, - DawnTime: times.Dawn, - NightTime: times.Night, - IsDay: isDay, - SunPosition: pos, - } - - m.stateMutex.Lock() - m.state = &newState - m.stateMutex.Unlock() - - m.notifySubscribers() -} - -func (m *Manager) notifier() { - defer m.notifierWg.Done() - const minGap = 100 * time.Millisecond - timer := time.NewTimer(minGap) - timer.Stop() - var pending bool - - for { - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.dirty: - if pending { - continue - } - pending = true - timer.Reset(minGap) - case <-timer.C: - if !pending { - continue - } - currentState := m.GetState() - if m.lastNotified != nil && !stateChanged(m.lastNotified, ¤tState) { - pending = false - continue - } - m.subscribers.Range(func(_ string, ch chan State) bool { - select { - case ch <- currentState: - default: - } - return true - }) - stateCopy := currentState - m.lastNotified = &stateCopy - pending = false - } - } -} - -func (m *Manager) dbusMonitor() { - defer m.wg.Done() - for { - select { - case <-m.stopChan: - return - case sig := <-m.dbusSignal: - if sig == nil { - continue - } - m.handleDBusSignal(sig) - } - } -} - -func (m *Manager) handleDBusSignal(sig *dbus.Signal) { - switch { - case sig.Name != "org.freedesktop.login1.Manager.PrepareForSleep": - return - case len(sig.Body) == 0: - return - } - preparing, ok := sig.Body[0].(bool) - if !ok || preparing { - return - } - m.configMutex.RLock() - enabled := m.config.Enabled - m.configMutex.RUnlock() - if !enabled { - return - } - time.AfterFunc(500*time.Millisecond, func() { - m.post(m.handleResume) - }) -} - -func (m *Manager) handleResume() { - m.configMutex.RLock() - stillEnabled := m.config.Enabled - m.configMutex.RUnlock() - - switch { - case !stillEnabled: - return - case !m.controlsInitialized: - return - case m.connectionDead.Load(): - return - } - - // Compositors (Niri, Hyprland, wlroots-based) re-apply the cached gamma - // ramp to DRM on resume; gamma_control objects stay valid. We just need - // to force a resend so the schedule catches up with the current time of - // day — the original #1235 regression was caused by lastAppliedTemp - // matching and the send being skipped. - m.recalcSchedule(time.Now()) - m.lastAppliedTemp = 0 - m.applyCurrentTemp("resume") -} - -func (m *Manager) triggerUpdate() { - select { - case m.updateTrigger <- struct{}{}: - default: - } -} - -func (m *Manager) SetConfig(config Config) error { - if err := config.Validate(); err != nil { - return err - } - m.configMutex.Lock() - m.config = config - m.configMutex.Unlock() - m.triggerUpdate() - return nil -} - -func (m *Manager) SetTemperature(low, high int) error { - m.configMutex.Lock() - if m.config.LowTemp == low && m.config.HighTemp == high { - m.configMutex.Unlock() - return nil - } - m.config.LowTemp = low - m.config.HighTemp = high - err := m.config.Validate() - m.configMutex.Unlock() - if err != nil { - return err - } - m.triggerUpdate() - return nil -} - -func (m *Manager) SetLocation(lat, lon float64) error { - m.configMutex.Lock() - if m.config.Latitude != nil && m.config.Longitude != nil && - *m.config.Latitude == lat && *m.config.Longitude == lon && !m.config.UseIPLocation { - 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 { - return err - } - m.triggerUpdate() - return nil -} - -func (m *Manager) SetUseIPLocation(use bool) { - m.configMutex.Lock() - if m.config.UseIPLocation == use { - m.configMutex.Unlock() - return - } - m.config.UseIPLocation = use - if use { - m.config.Latitude = nil - m.config.Longitude = nil - } - m.configMutex.Unlock() - - if use { - m.locationMutex.Lock() - m.cachedIPLat = nil - m.cachedIPLon = nil - m.locationMutex.Unlock() - } - m.triggerUpdate() -} - -func (m *Manager) SetManualTimes(sunrise, sunset time.Time) 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.configMutex.Unlock() - return nil - } - m.config.ManualSunrise = &sunrise - m.config.ManualSunset = &sunset - err := m.config.Validate() - m.configMutex.Unlock() - if err != nil { - return err - } - m.triggerUpdate() - return nil -} - -func (m *Manager) ClearManualTimes() { - m.configMutex.Lock() - if m.config.ManualSunrise == nil && m.config.ManualSunset == nil { - m.configMutex.Unlock() - return - } - m.config.ManualSunrise = nil - m.config.ManualSunset = nil - m.configMutex.Unlock() - m.triggerUpdate() -} - -func (m *Manager) SetGamma(gamma float64) error { - m.configMutex.Lock() - if m.config.Gamma == gamma { - m.configMutex.Unlock() - return nil - } - m.config.Gamma = gamma - err := m.config.Validate() - m.configMutex.Unlock() - if err != nil { - return err - } - m.triggerUpdate() - return nil -} - -func (m *Manager) SetEnabled(enabled bool) { - m.configMutex.Lock() - wasEnabled := m.config.Enabled - if wasEnabled == enabled { - m.configMutex.Unlock() - return - } - m.config.Enabled = enabled - highTemp := m.config.HighTemp - m.configMutex.Unlock() - - switch { - case enabled && !m.controlsInitialized: - m.post(func() { - gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) - m.availOutputsMu.RLock() - outs := slices.Clone(m.availableOutputs) - m.availOutputsMu.RUnlock() - if err := m.setupOutputControls(outs, gammaMgr); err != nil { - log.Errorf("gamma: failed to create controls: %v", err) - return - } - m.controlsInitialized = true - m.triggerUpdate() - }) - case enabled && !wasEnabled: - m.triggerUpdate() - case !enabled && m.controlsInitialized: - m.post(func() { - m.outputs.Range(func(id uint32, out *outputState) bool { - if out.gammaControl != nil { - out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy() - } - return true - }) - m.outputs.Range(func(key uint32, _ *outputState) bool { - m.outputs.Delete(key) - return true - }) - m.controlsInitialized = false - }) - _ = highTemp - } -} - -func (m *Manager) Close() { - close(m.stopChan) - m.wg.Wait() - m.notifierWg.Wait() - - m.subscribers.Range(func(key string, ch chan State) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - - m.outputs.Range(func(_ uint32, out *outputState) bool { - if ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok { - ctrl.Destroy() - } - return true - }) - m.outputs.Range(func(key uint32, _ *outputState) bool { - m.outputs.Delete(key) - return true - }) - - if manager, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1); ok { - manager.Destroy() - } - - if m.dbusConn != nil { - m.dbusConn.RemoveSignal(m.dbusSignal) - m.dbusConn.Close() - } -} - -func MemfdCreate(name string, flags int) (int, error) { - fd, err := unix.MemfdCreate(name, flags) - if err != nil { - return -1, err - } - return fd, nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager_test.go deleted file mode 100644 index 8c00129..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/manager_test.go +++ /dev/null @@ -1,414 +0,0 @@ -package wayland - -import ( - "errors" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" -) - -func TestManager_ActorSerializesOutputStateAccess(t *testing.T) { - m := &Manager{ - cmdq: make(chan cmd, 8192), - stopChan: make(chan struct{}), - } - - m.wg.Add(1) - go m.waylandActor() - - state := &outputState{ - id: 1, - registryName: 100, - rampSize: 256, - } - m.outputs.Store(state.id, state) - - var wg sync.WaitGroup - const goroutines = 50 - const iterations = 100 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.post(func() { - if out, ok := m.outputs.Load(state.id); ok { - out.rampSize = uint32(j) - out.failed = j%2 == 0 - out.retryCount = j - out.lastFailTime = time.Now() - } - }) - } - }(i) - } - - wg.Wait() - - done := make(chan struct{}) - m.post(func() { close(done) }) - <-done - - close(m.stopChan) - m.wg.Wait() -} - -func TestManager_ConcurrentSubscriberAccess(t *testing.T) { - m := &Manager{ - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - updateTrigger: make(chan struct{}, 1), - } - - var wg sync.WaitGroup - const goroutines = 20 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - subID := string(rune('a' + id)) - ch := m.Subscribe(subID) - assert.NotNil(t, ch) - time.Sleep(time.Millisecond) - m.Unsubscribe(subID) - }(i) - } - - wg.Wait() -} - -func TestManager_ConcurrentGetState(t *testing.T) { - m := &Manager{ - state: &State{ - CurrentTemp: 5000, - IsDay: true, - }, - } - - var wg sync.WaitGroup - const goroutines = 50 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - s := m.GetState() - assert.GreaterOrEqual(t, s.CurrentTemp, 0) - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.stateMutex.Lock() - m.state = &State{ - CurrentTemp: 4000 + i*100, - IsDay: j%2 == 0, - } - m.stateMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_ConcurrentConfigAccess(t *testing.T) { - m := &Manager{ - config: DefaultConfig(), - } - - var wg sync.WaitGroup - const goroutines = 30 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.configMutex.RLock() - _ = m.config.LowTemp - _ = m.config.HighTemp - _ = m.config.Enabled - m.configMutex.RUnlock() - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.configMutex.Lock() - m.config.LowTemp = 3000 + j - m.config.HighTemp = 7000 - j - m.config.Enabled = j%2 == 0 - m.configMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_SyncmapOutputsConcurrentAccess(t *testing.T) { - m := &Manager{} - - var wg sync.WaitGroup - const goroutines = 30 - const iterations = 50 - - for i := 0; i < goroutines; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - key := uint32(id) - - for j := 0; j < iterations; j++ { - state := &outputState{ - id: key, - rampSize: uint32(j), - failed: j%2 == 0, - } - m.outputs.Store(key, state) - - if loaded, ok := m.outputs.Load(key); ok { - assert.Equal(t, key, loaded.id) - } - - m.outputs.Range(func(k uint32, v *outputState) bool { - _ = v.rampSize - _ = v.failed - return true - }) - } - - m.outputs.Delete(key) - }(i) - } - - wg.Wait() -} - -func TestManager_LocationCacheConcurrentAccess(t *testing.T) { - m := &Manager{} - - var wg sync.WaitGroup - const goroutines = 20 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.locationMutex.RLock() - _ = m.cachedIPLat - _ = m.cachedIPLon - m.locationMutex.RUnlock() - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := 0; j < iterations; j++ { - lat := float64(40 + i) - lon := float64(-74 + j) - m.locationMutex.Lock() - m.cachedIPLat = &lat - m.cachedIPLon = &lon - m.locationMutex.Unlock() - } - }(i) - } - - wg.Wait() -} - -func TestManager_ScheduleConcurrentAccess(t *testing.T) { - now := time.Now() - m := &Manager{ - schedule: sunSchedule{ - times: SunTimes{ - Dawn: now, - Sunrise: now.Add(time.Hour), - Sunset: now.Add(12 * time.Hour), - Night: now.Add(13 * time.Hour), - }, - }, - } - - var wg sync.WaitGroup - const goroutines = 20 - const iterations = 100 - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.scheduleMutex.RLock() - _ = m.schedule.times.Dawn - _ = m.schedule.times.Sunrise - _ = m.schedule.times.Sunset - _ = m.schedule.condition - m.scheduleMutex.RUnlock() - } - }() - } - - for i := 0; i < goroutines/2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < iterations; j++ { - m.scheduleMutex.Lock() - m.schedule.times.Dawn = time.Now() - m.schedule.times.Sunrise = time.Now().Add(time.Hour) - m.schedule.condition = SunNormal - m.scheduleMutex.Unlock() - } - }() - } - - wg.Wait() -} - -func TestInterpolate_EdgeCases(t *testing.T) { - now := time.Now() - - tests := []struct { - name string - now time.Time - start time.Time - stop time.Time - expected float64 - }{ - { - name: "same start and stop", - now: now, - start: now, - stop: now, - expected: 1.0, - }, - { - name: "now before start", - now: now, - start: now.Add(time.Hour), - stop: now.Add(2 * time.Hour), - expected: 0.0, - }, - { - name: "now after stop", - now: now.Add(3 * time.Hour), - start: now, - stop: now.Add(time.Hour), - expected: 1.0, - }, - { - name: "now at midpoint", - now: now.Add(30 * time.Minute), - start: now, - stop: now.Add(time.Hour), - expected: 0.5, - }, - { - name: "now equals start", - now: now, - start: now, - stop: now.Add(time.Hour), - expected: 0.0, - }, - { - name: "now equals stop", - now: now.Add(time.Hour), - start: now, - stop: now.Add(time.Hour), - expected: 1.0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := interpolate(tt.now, tt.start, tt.stop) - assert.InDelta(t, tt.expected, result, 0.01) - }) - } -} - -func TestGenerateGammaRamp_ZeroSize(t *testing.T) { - ramp := GenerateGammaRamp(0, 5000, 1.0) - assert.Empty(t, ramp.Red) - assert.Empty(t, ramp.Green) - assert.Empty(t, ramp.Blue) -} - -func TestGenerateGammaRamp_ValidSizes(t *testing.T) { - sizes := []uint32{1, 256, 1024} - temps := []int{1000, 4000, 6500, 10000} - gammas := []float64{0.5, 1.0, 2.0} - - for _, size := range sizes { - for _, temp := range temps { - for _, gamma := range gammas { - ramp := GenerateGammaRamp(size, temp, gamma) - assert.Len(t, ramp.Red, int(size)) - assert.Len(t, ramp.Green, int(size)) - assert.Len(t, ramp.Blue, int(size)) - } - } - } -} - -func TestNotifySubscribers_NonBlocking(t *testing.T) { - m := &Manager{ - dirty: make(chan struct{}, 1), - } - - for i := 0; i < 10; i++ { - m.notifySubscribers() - } - - assert.Len(t, m.dirty, 1) -} - -func TestNewManager_GetRegistryError(t *testing.T) { - mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) - - mockDisplay.EXPECT().Context().Return(nil) - mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry")) - - config := DefaultConfig() - _, err := NewManager(mockDisplay, config) - assert.Error(t, err) - assert.Contains(t, err.Error(), "get registry") -} - -func TestNewManager_InvalidConfig(t *testing.T) { - mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) - - config := Config{ - LowTemp: 500, - HighTemp: 6500, - Gamma: 1.0, - } - - _, err := NewManager(mockDisplay, config) - assert.Error(t, err) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc.go deleted file mode 100644 index c978a8e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc.go +++ /dev/null @@ -1,122 +0,0 @@ -package wayland - -import ( - "math" - "time" -) - -const ( - degToRad = math.Pi / 180.0 - radToDeg = 180.0 / math.Pi -) - -type SunCondition int - -const ( - SunNormal SunCondition = iota - SunMidnightSun - SunPolarNight -) - -type SunTimes struct { - Dawn time.Time - Sunrise time.Time - Sunset time.Time - Night time.Time -} - -func daysInYear(year int) int { - if (year%4 == 0 && year%100 != 0) || year%400 == 0 { - return 366 - } - return 365 -} - -func dateOrbitAngle(t time.Time) float64 { - return 2 * math.Pi / float64(daysInYear(t.Year())) * float64(t.YearDay()-1) -} - -func equationOfTime(orbitAngle float64) float64 { - return 4 * (0.000075 + - 0.001868*math.Cos(orbitAngle) - - 0.032077*math.Sin(orbitAngle) - - 0.014615*math.Cos(2*orbitAngle) - - 0.040849*math.Sin(2*orbitAngle)) -} - -func sunDeclination(orbitAngle float64) float64 { - return 0.006918 - - 0.399912*math.Cos(orbitAngle) + - 0.070257*math.Sin(orbitAngle) - - 0.006758*math.Cos(2*orbitAngle) + - 0.000907*math.Sin(2*orbitAngle) - - 0.002697*math.Cos(3*orbitAngle) + - 0.00148*math.Sin(3*orbitAngle) -} - -func sunHourAngle(latRad, declination, targetSunRad float64) float64 { - return math.Acos(math.Cos(targetSunRad)/ - math.Cos(latRad)*math.Cos(declination) - - math.Tan(latRad)*math.Tan(declination)) -} - -func hourAngleToSeconds(hourAngle, eqtime float64) float64 { - return radToDeg * (4.0*math.Pi - 4*hourAngle - eqtime) * 60 -} - -func sunCondition(latRad, declination float64) SunCondition { - signLat := latRad >= 0 - signDecl := declination >= 0 - if signLat == signDecl { - return SunMidnightSun - } - return SunPolarNight -} - -func CalculateSunTimesWithTwilight(lat, lon float64, date time.Time, elevTwilight, elevDaylight float64) (SunTimes, SunCondition) { - latRad := lat * degToRad - elevTwilightRad := (90.833 - elevTwilight) * degToRad - elevDaylightRad := (90.833 - elevDaylight) * degToRad - - utc := date.UTC() - orbitAngle := dateOrbitAngle(utc) - decl := sunDeclination(orbitAngle) - eqtime := equationOfTime(orbitAngle) - - haTwilight := sunHourAngle(latRad, decl, elevTwilightRad) - haDaylight := sunHourAngle(latRad, decl, elevDaylightRad) - - if math.IsNaN(haTwilight) || math.IsNaN(haDaylight) { - cond := sunCondition(latRad, decl) - return SunTimes{}, cond - } - - dayStart := time.Date(utc.Year(), utc.Month(), utc.Day(), 0, 0, 0, 0, time.UTC) - lonOffset := time.Duration(-lon*4) * time.Minute - - dawnSecs := hourAngleToSeconds(math.Abs(haTwilight), eqtime) - sunriseSecs := hourAngleToSeconds(math.Abs(haDaylight), eqtime) - sunsetSecs := hourAngleToSeconds(-math.Abs(haDaylight), eqtime) - nightSecs := hourAngleToSeconds(-math.Abs(haTwilight), eqtime) - - return SunTimes{ - Dawn: dayStart.Add(time.Duration(dawnSecs)*time.Second + lonOffset).In(date.Location()), - Sunrise: dayStart.Add(time.Duration(sunriseSecs)*time.Second + lonOffset).In(date.Location()), - Sunset: dayStart.Add(time.Duration(sunsetSecs)*time.Second + lonOffset).In(date.Location()), - Night: dayStart.Add(time.Duration(nightSecs)*time.Second + lonOffset).In(date.Location()), - }, SunNormal -} - -func CalculateSunTimes(lat, lon float64, date time.Time) SunTimes { - times, cond := CalculateSunTimesWithTwilight(lat, lon, date, -6.0, 3.0) - switch cond { - case SunMidnightSun: - dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location()) - dayEnd := dayStart.Add(24*time.Hour - time.Second) - return SunTimes{Dawn: dayStart, Sunrise: dayStart, Sunset: dayEnd, Night: dayEnd} - case SunPolarNight: - dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location()) - return SunTimes{Dawn: dayStart, Sunrise: dayStart, Sunset: dayStart, Night: dayStart} - } - return times -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc_test.go deleted file mode 100644 index b2815d9..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/suncalc_test.go +++ /dev/null @@ -1,387 +0,0 @@ -package wayland - -import ( - "math" - "testing" - "time" -) - -func calculateTemperature(config Config, now time.Time) int { - if !config.Enabled { - return config.HighTemp - } - - var sunrise, sunset time.Time - - if config.ManualSunrise != nil && config.ManualSunset != nil { - year, month, day := now.Date() - loc := now.Location() - - sunrise = time.Date(year, month, day, - config.ManualSunrise.Hour(), - config.ManualSunrise.Minute(), - config.ManualSunrise.Second(), 0, loc) - sunset = time.Date(year, month, day, - config.ManualSunset.Hour(), - config.ManualSunset.Minute(), - config.ManualSunset.Second(), 0, loc) - - if sunset.Before(sunrise) { - sunset = sunset.Add(24 * time.Hour) - } - } else if config.UseIPLocation { - lat, lon, err := FetchIPLocation() - if err != nil { - return config.HighTemp - } - times := CalculateSunTimes(*lat, *lon, now) - sunrise = times.Sunrise - sunset = times.Sunset - } else if config.Latitude != nil && config.Longitude != nil { - times := CalculateSunTimes(*config.Latitude, *config.Longitude, now) - sunrise = times.Sunrise - sunset = times.Sunset - } else { - return config.HighTemp - } - - if now.Before(sunrise) || now.After(sunset) { - return config.LowTemp - } - return config.HighTemp -} - -func calculateNextTransition(config Config, now time.Time) time.Time { - if !config.Enabled { - return now.Add(24 * time.Hour) - } - - var sunrise, sunset time.Time - - if config.ManualSunrise != nil && config.ManualSunset != nil { - year, month, day := now.Date() - loc := now.Location() - - sunrise = time.Date(year, month, day, - config.ManualSunrise.Hour(), - config.ManualSunrise.Minute(), - config.ManualSunrise.Second(), 0, loc) - sunset = time.Date(year, month, day, - config.ManualSunset.Hour(), - config.ManualSunset.Minute(), - config.ManualSunset.Second(), 0, loc) - - if sunset.Before(sunrise) { - sunset = sunset.Add(24 * time.Hour) - } - } else if config.UseIPLocation { - lat, lon, err := FetchIPLocation() - if err != nil { - return now.Add(24 * time.Hour) - } - times := CalculateSunTimes(*lat, *lon, now) - sunrise = times.Sunrise - sunset = times.Sunset - } else if config.Latitude != nil && config.Longitude != nil { - times := CalculateSunTimes(*config.Latitude, *config.Longitude, now) - sunrise = times.Sunrise - sunset = times.Sunset - } else { - return now.Add(24 * time.Hour) - } - - if now.Before(sunrise) { - return sunrise - } - if now.Before(sunset) { - return sunset - } - - if config.ManualSunrise != nil && config.ManualSunset != nil { - year, month, day := now.Add(24 * time.Hour).Date() - loc := now.Location() - nextSunrise := time.Date(year, month, day, - config.ManualSunrise.Hour(), - config.ManualSunrise.Minute(), - config.ManualSunrise.Second(), 0, loc) - return nextSunrise - } - - if config.UseIPLocation { - lat, lon, err := FetchIPLocation() - if err != nil { - return now.Add(24 * time.Hour) - } - nextDayTimes := CalculateSunTimes(*lat, *lon, now.Add(24*time.Hour)) - return nextDayTimes.Sunrise - } - - if config.Latitude != nil && config.Longitude != nil { - nextDayTimes := CalculateSunTimes(*config.Latitude, *config.Longitude, now.Add(24*time.Hour)) - return nextDayTimes.Sunrise - } - - return now.Add(24 * time.Hour) -} - -func TestCalculateSunTimes(t *testing.T) { - tests := []struct { - name string - lat float64 - lon float64 - date time.Time - checkFunc func(*testing.T, SunTimes) - }{ - { - name: "new_york_summer", - lat: 40.7128, - lon: -74.0060, - date: time.Date(2024, 6, 21, 12, 0, 0, 0, time.Local), - checkFunc: func(t *testing.T, times SunTimes) { - if times.Sunrise.Hour() < 4 || times.Sunrise.Hour() > 6 { - t.Logf("sunrise: %v", times.Sunrise) - } - if times.Sunset.Hour() < 19 || times.Sunset.Hour() > 21 { - t.Logf("sunset: %v", times.Sunset) - } - if !times.Sunset.After(times.Sunrise) { - t.Error("sunset should be after sunrise") - } - }, - }, - { - name: "london_winter", - lat: 51.5074, - lon: -0.1278, - date: time.Date(2024, 12, 21, 12, 0, 0, 0, time.UTC), - checkFunc: func(t *testing.T, times SunTimes) { - if times.Sunrise.Hour() < 7 || times.Sunrise.Hour() > 9 { - t.Errorf("unexpected sunrise hour: %d", times.Sunrise.Hour()) - } - if times.Sunset.Hour() < 15 || times.Sunset.Hour() > 17 { - t.Errorf("unexpected sunset hour: %d", times.Sunset.Hour()) - } - }, - }, - { - name: "equator_equinox", - lat: 0.0, - lon: 0.0, - date: time.Date(2024, 3, 20, 12, 0, 0, 0, time.UTC), - checkFunc: func(t *testing.T, times SunTimes) { - if times.Sunrise.Hour() < 5 || times.Sunrise.Hour() > 7 { - t.Errorf("unexpected sunrise hour: %d", times.Sunrise.Hour()) - } - if times.Sunset.Hour() < 17 || times.Sunset.Hour() > 19 { - t.Errorf("unexpected sunset hour: %d", times.Sunset.Hour()) - } - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - times := CalculateSunTimes(tt.lat, tt.lon, tt.date) - tt.checkFunc(t, times) - }) - } -} - -func TestCalculateTemperature(t *testing.T) { - lat := 40.7128 - lon := -74.0060 - date := time.Date(2024, 6, 21, 0, 0, 0, 0, time.Local) - - config := Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: &lat, - Longitude: &lon, - Enabled: true, - } - - times := CalculateSunTimes(lat, lon, date) - - tests := []struct { - name string - timeFunc func() time.Time - wantTemp int - }{ - { - name: "midnight", - timeFunc: func() time.Time { return times.Sunrise.Add(-4 * time.Hour) }, - wantTemp: 4000, - }, - { - name: "sunrise", - timeFunc: func() time.Time { return times.Sunrise }, - wantTemp: 6500, - }, - { - name: "noon", - timeFunc: func() time.Time { return times.Sunrise.Add(6 * time.Hour) }, - wantTemp: 6500, - }, - { - name: "after_sunset_transition", - timeFunc: func() time.Time { return times.Sunset.Add(2 * time.Hour) }, - wantTemp: 4000, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - temp := calculateTemperature(config, tt.timeFunc()) - - if math.Abs(float64(temp-tt.wantTemp)) > 500 { - t.Errorf("temperature = %d, want approximately %d", temp, tt.wantTemp) - } - }) - } -} - -func TestCalculateTemperatureManualTimes(t *testing.T) { - sunrise := time.Date(0, 1, 1, 6, 30, 0, 0, time.Local) - sunset := time.Date(0, 1, 1, 18, 30, 0, 0, time.Local) - - config := Config{ - LowTemp: 4000, - HighTemp: 6500, - ManualSunrise: &sunrise, - ManualSunset: &sunset, - Enabled: true, - } - - tests := []struct { - name string - time time.Time - want int - }{ - {"before_sunrise", time.Date(2024, 1, 1, 3, 0, 0, 0, time.Local), 4000}, - {"at_sunrise", time.Date(2024, 1, 1, 6, 30, 0, 0, time.Local), 6500}, - {"midday", time.Date(2024, 1, 1, 12, 0, 0, 0, time.Local), 6500}, - {"at_sunset", time.Date(2024, 1, 1, 18, 30, 0, 0, time.Local), 6500}, - {"after_sunset", time.Date(2024, 1, 1, 22, 0, 0, 0, time.Local), 4000}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - temp := calculateTemperature(config, tt.time) - if math.Abs(float64(temp-tt.want)) > 500 { - t.Errorf("temperature = %d, want approximately %d", temp, tt.want) - } - }) - } -} - -func TestCalculateTemperatureDisabled(t *testing.T) { - lat := 40.7128 - lon := -74.0060 - - config := Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: &lat, - Longitude: &lon, - Enabled: false, - } - - temp := calculateTemperature(config, time.Now()) - if temp != 6500 { - t.Errorf("disabled should return high temp, got %d", temp) - } -} - -func TestCalculateNextTransition(t *testing.T) { - lat := 40.7128 - lon := -74.0060 - date := time.Date(2024, 6, 21, 0, 0, 0, 0, time.Local) - - config := Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: &lat, - Longitude: &lon, - Enabled: true, - } - - times := CalculateSunTimes(lat, lon, date) - - tests := []struct { - name string - now time.Time - checkFunc func(*testing.T, time.Time) - }{ - { - name: "before_sunrise", - now: times.Sunrise.Add(-2 * time.Hour), - checkFunc: func(t *testing.T, next time.Time) { - if !next.Equal(times.Sunrise) && !next.After(times.Sunrise.Add(-1*time.Minute)) { - t.Error("next transition should be at or near sunrise") - } - }, - }, - { - name: "after_sunrise", - now: times.Sunrise.Add(2 * time.Hour), - checkFunc: func(t *testing.T, next time.Time) { - if !next.After(times.Sunrise) { - t.Error("next transition should be after sunrise") - } - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - next := calculateNextTransition(config, tt.now) - tt.checkFunc(t, next) - }) - } -} - -func TestSunTimesWithTwilight(t *testing.T) { - lat := 40.7128 - lon := -74.0060 - date := time.Date(2024, 6, 21, 12, 0, 0, 0, time.Local) - - times, cond := CalculateSunTimesWithTwilight(lat, lon, date, -6.0, 3.0) - - if cond != SunNormal { - t.Errorf("expected SunNormal, got %v", cond) - } - if !times.Dawn.Before(times.Sunrise) { - t.Error("dawn should be before sunrise") - } - if !times.Sunrise.Before(times.Sunset) { - t.Error("sunrise should be before sunset") - } - if !times.Sunset.Before(times.Night) { - t.Error("sunset should be before night") - } -} - -func TestSunConditions(t *testing.T) { - tests := []struct { - name string - lat float64 - date time.Time - expected SunCondition - }{ - { - name: "normal_conditions", - lat: 40.0, - date: time.Date(2024, 6, 21, 12, 0, 0, 0, time.UTC), - expected: SunNormal, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, cond := CalculateSunTimesWithTwilight(tt.lat, 0, tt.date, -6.0, 3.0) - if cond != tt.expected { - t.Errorf("expected condition %v, got %v", tt.expected, cond) - } - }) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types.go deleted file mode 100644 index 31bac15..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types.go +++ /dev/null @@ -1,216 +0,0 @@ -package wayland - -import ( - "math" - "sync" - "sync/atomic" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation" - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" - "github.com/godbus/dbus/v5" -) - -type GammaState int - -const ( - StateNormal GammaState = iota - StateTransition - StateStatic -) - -type Config struct { - Outputs []string - LowTemp int - HighTemp int - Latitude *float64 - Longitude *float64 - UseIPLocation bool - ManualSunrise *time.Time - ManualSunset *time.Time - ManualDuration *time.Duration - Gamma float64 - Enabled bool - ElevationTwilight float64 - ElevationDaylight float64 -} - -type State struct { - Config Config `json:"config"` - CurrentTemp int `json:"currentTemp"` - NextTransition time.Time `json:"nextTransition"` - SunriseTime time.Time `json:"sunriseTime"` - SunsetTime time.Time `json:"sunsetTime"` - DawnTime time.Time `json:"dawnTime"` - NightTime time.Time `json:"nightTime"` - IsDay bool `json:"isDay"` - SunPosition float64 `json:"sunPosition"` -} - -type cmd struct { - fn func() -} - -type sunSchedule struct { - times SunTimes - condition SunCondition - dawnStepTime time.Duration - nightStepTime time.Duration - calcDay time.Time -} - -type Manager struct { - config Config - configMutex sync.RWMutex - state *State - stateMutex sync.RWMutex - - display wlclient.WaylandDisplay - ctx *wlclient.Context - registry *wlclient.Registry - gammaControl any - availableOutputs []*wlclient.Output - availOutputsMu sync.RWMutex - outputRegNames syncmap.Map[uint32, uint32] - outputs syncmap.Map[uint32, *outputState] - controlsInitialized bool - connectionDead atomic.Bool - - cmdq chan cmd - alive bool - - stopChan chan struct{} - updateTrigger chan struct{} - wg sync.WaitGroup - - schedule sunSchedule - scheduleMutex sync.RWMutex - gammaState GammaState - - cachedIPLat *float64 - cachedIPLon *float64 - locationMutex sync.RWMutex - - subscribers syncmap.Map[string, chan State] - dirty chan struct{} - notifierWg sync.WaitGroup - lastNotified *State - - dbusConn *dbus.Conn - dbusSignal chan *dbus.Signal - - geoClient geolocation.Client - - lastAppliedTemp int - lastAppliedGamma float64 -} - -type outputState struct { - id uint32 - registryName uint32 - output *wlclient.Output - gammaControl any - rampSize uint32 - failed bool - isVirtual bool - retryCount int - lastFailTime time.Time -} - -func DefaultConfig() Config { - return Config{ - Outputs: []string{}, - LowTemp: 4000, - HighTemp: 6500, - Gamma: 1.0, - Enabled: false, - ElevationTwilight: -6.0, - ElevationDaylight: 3.0, - } -} - -func (c *Config) Validate() error { - if c.LowTemp < 1000 || c.LowTemp > 10000 { - return errdefs.ErrInvalidTemperature - } - if c.HighTemp < 1000 || c.HighTemp > 10000 { - return errdefs.ErrInvalidTemperature - } - if c.LowTemp > c.HighTemp { - return errdefs.ErrInvalidTemperature - } - if c.Gamma <= 0 || c.Gamma > 10 { - return errdefs.ErrInvalidGamma - } - if c.Latitude != nil && (math.Abs(*c.Latitude) > 90) { - return errdefs.ErrInvalidLocation - } - if c.Longitude != nil && (math.Abs(*c.Longitude) > 180) { - return errdefs.ErrInvalidLocation - } - if (c.Latitude != nil) != (c.Longitude != nil) { - return errdefs.ErrInvalidLocation - } - if (c.ManualSunrise != nil) != (c.ManualSunset != nil) { - return errdefs.ErrInvalidManualTimes - } - return nil -} - -func (m *Manager) GetState() State { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - if m.state == nil { - return State{} - } - return *m.state -} - -func (m *Manager) Subscribe(id string) chan State { - ch := make(chan State, 64) - m.subscribers.Store(id, ch) - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) notifySubscribers() { - select { - case m.dirty <- struct{}{}: - default: - } -} - -func stateChanged(old, new *State) bool { - if old == nil || new == nil { - return true - } - if old.CurrentTemp != new.CurrentTemp { - return true - } - if old.IsDay != new.IsDay { - return true - } - if !old.NextTransition.Equal(new.NextTransition) { - return true - } - if !old.SunriseTime.Equal(new.SunriseTime) { - return true - } - if !old.SunsetTime.Equal(new.SunsetTime) { - return true - } - if old.Config.Enabled != new.Config.Enabled { - return true - } - if old.SunPosition != new.SunPosition { - return true - } - return false -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types_test.go deleted file mode 100644 index f5bbb42..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wayland/types_test.go +++ /dev/null @@ -1,330 +0,0 @@ -package wayland - -import ( - "testing" - "time" -) - -func TestConfigValidate(t *testing.T) { - tests := []struct { - name string - config Config - wantErr bool - }{ - { - name: "valid_default", - config: DefaultConfig(), - wantErr: false, - }, - { - name: "valid_with_location", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: floatPtr(40.7128), - Longitude: floatPtr(-74.0060), - Gamma: 1.0, - Enabled: true, - }, - wantErr: false, - }, - { - name: "valid_manual_times", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - ManualSunrise: timePtr(time.Date(0, 1, 1, 6, 30, 0, 0, time.Local)), - ManualSunset: timePtr(time.Date(0, 1, 1, 18, 30, 0, 0, time.Local)), - Gamma: 1.0, - Enabled: true, - }, - wantErr: false, - }, - { - name: "invalid_low_temp_too_low", - config: Config{ - LowTemp: 500, - HighTemp: 6500, - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_low_temp_too_high", - config: Config{ - LowTemp: 15000, - HighTemp: 20000, - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_high_temp_too_low", - config: Config{ - LowTemp: 4000, - HighTemp: 500, - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "valid_temps_equal", - config: Config{ - LowTemp: 5000, - HighTemp: 5000, - Gamma: 1.0, - }, - wantErr: false, - }, - { - name: "invalid_temps_reversed", - config: Config{ - LowTemp: 6500, - HighTemp: 4000, - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_gamma_zero", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Gamma: 0, - }, - wantErr: true, - }, - { - name: "invalid_gamma_negative", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Gamma: -1.0, - }, - wantErr: true, - }, - { - name: "invalid_gamma_too_high", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Gamma: 15.0, - }, - wantErr: true, - }, - { - name: "invalid_latitude_too_high", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: floatPtr(100), - Longitude: floatPtr(0), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_latitude_too_low", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: floatPtr(-100), - Longitude: floatPtr(0), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_longitude_too_high", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: floatPtr(40), - Longitude: floatPtr(200), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_longitude_too_low", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: floatPtr(40), - Longitude: floatPtr(-200), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_latitude_without_longitude", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Latitude: floatPtr(40), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_longitude_without_latitude", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Longitude: floatPtr(-74), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_sunrise_without_sunset", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - ManualSunrise: timePtr(time.Date(0, 1, 1, 6, 30, 0, 0, time.Local)), - Gamma: 1.0, - }, - wantErr: true, - }, - { - name: "invalid_sunset_without_sunrise", - config: Config{ - LowTemp: 4000, - HighTemp: 6500, - ManualSunset: timePtr(time.Date(0, 1, 1, 18, 30, 0, 0, time.Local)), - Gamma: 1.0, - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.config.Validate() - if (err != nil) != tt.wantErr { - t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestDefaultConfig(t *testing.T) { - config := DefaultConfig() - - if config.LowTemp != 4000 { - t.Errorf("default low temp = %d, want 4000", config.LowTemp) - } - if config.HighTemp != 6500 { - t.Errorf("default high temp = %d, want 6500", config.HighTemp) - } - if config.Gamma != 1.0 { - t.Errorf("default gamma = %f, want 1.0", config.Gamma) - } - if config.Enabled { - t.Error("default should be disabled") - } - if config.Latitude != nil { - t.Error("default should not have latitude") - } - if config.Longitude != nil { - t.Error("default should not have longitude") - } -} - -func TestStateChanged(t *testing.T) { - baseState := &State{ - CurrentTemp: 5000, - NextTransition: time.Now(), - SunriseTime: time.Now().Add(6 * time.Hour), - SunsetTime: time.Now().Add(18 * time.Hour), - IsDay: true, - Config: DefaultConfig(), - } - - tests := []struct { - name string - old *State - new *State - wantChanged bool - }{ - { - name: "nil_old", - old: nil, - new: baseState, - wantChanged: true, - }, - { - name: "nil_new", - old: baseState, - new: nil, - wantChanged: true, - }, - { - name: "same_state", - old: baseState, - new: baseState, - wantChanged: false, - }, - { - name: "temp_changed", - old: baseState, - new: &State{ - CurrentTemp: 6000, - NextTransition: baseState.NextTransition, - SunriseTime: baseState.SunriseTime, - SunsetTime: baseState.SunsetTime, - IsDay: baseState.IsDay, - Config: baseState.Config, - }, - wantChanged: true, - }, - { - name: "is_day_changed", - old: baseState, - new: &State{ - CurrentTemp: baseState.CurrentTemp, - NextTransition: baseState.NextTransition, - SunriseTime: baseState.SunriseTime, - SunsetTime: baseState.SunsetTime, - IsDay: false, - Config: baseState.Config, - }, - wantChanged: true, - }, - { - name: "enabled_changed", - old: baseState, - new: &State{ - CurrentTemp: baseState.CurrentTemp, - NextTransition: baseState.NextTransition, - SunriseTime: baseState.SunriseTime, - SunsetTime: baseState.SunsetTime, - IsDay: baseState.IsDay, - Config: Config{ - LowTemp: 4000, - HighTemp: 6500, - Gamma: 1.0, - Enabled: true, - }, - }, - wantChanged: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - changed := stateChanged(tt.old, tt.new) - if changed != tt.wantChanged { - t.Errorf("stateChanged() = %v, want %v", changed, tt.wantChanged) - } - }) - } -} - -func floatPtr(f float64) *float64 { - return &f -} - -func timePtr(t time.Time) *time.Time { - return &t -} |