package wayland import ( "bytes" "encoding/binary" "fmt" "os" "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/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control" ) func NewManager(display *wlclient.Display, config Config) (*Manager, error) { if err := config.Validate(); err != nil { return nil, err } 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), transitionChan: make(chan int, 1), } if err := m.setupRegistry(); err != nil { return nil, err } // Setup D-Bus monitoring for suspend/resume events if err := m.setupDBusMonitor(); err != nil { log.Warnf("Failed to setup D-Bus monitoring for suspend/resume: %v", err) // Don't fail initialization if D-Bus setup fails, just continue without it } // Initialize currentTemp and targetTemp before starting any goroutines now := time.Now() initial := m.calculateTemperature(now) m.transitionMutex.Lock() m.currentTemp = initial m.targetTemp = initial m.transitionMutex.Unlock() m.alive = true m.updateState() m.notifierWg.Add(1) go m.notifier() m.wg.Add(1) go m.updateLoop() if m.dbusConn != nil { m.wg.Add(1) go m.dbusMonitor() } m.wg.Add(1) go m.waylandActor() m.wg.Add(1) go m.transitionWorker() if config.Enabled { m.post(func() { log.Info("Gamma control enabled at startup, initializing controls") gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) if err := func() error { var outputs []*wlclient.Output = m.availableOutputs return m.setupOutputControls(outputs, gammaMgr) }(); err != nil { log.Errorf("Failed to initialize gamma controls: %v", err) } else { 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, dropping command") } } func (m *Manager) waylandActor() { defer m.wg.Done() for { select { case <-m.stopChan: return case c := <-m.cmdq: c.fn() } } } func (m *Manager) allOutputsReady() bool { hasOutputs := false allReady := true m.outputs.Range(func(key uint32, value *outputState) bool { hasOutputs = true if value.rampSize == 0 || value.failed { allReady = false return false } return true }) return hasOutputs && allReady } func (m *Manager) setupDBusMonitor() error { conn, err := dbus.ConnectSystemBus() if err != nil { return fmt.Errorf("failed to connect to system bus: %w", err) } // Subscribe to PrepareForSleep signal 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("failed to add match rule: %w", err) } conn.Signal(m.dbusSignal) m.dbusConn = conn log.Info("D-Bus monitoring for suspend/resume events enabled") return nil } func (m *Manager) setupRegistry() error { log.Info("setupRegistry: starting registry setup") registry, err := m.display.GetRegistry() if err != nil { return fmt.Errorf("failed to 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: log.Infof("setupRegistry: found %s", 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 log.Info("setupRegistry: gamma control manager bound successfully") } else { log.Errorf("setupRegistry: failed to bind gamma control: %v", err) } case "wl_output": log.Debugf("Global event: found wl_output (name=%d)", e.Name) 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 { outputID := output.ID() log.Infof("Bound wl_output id=%d registry_name=%d", outputID, e.Name) output.SetNameHandler(func(ev wlclient.OutputNameEvent) { log.Infof("Output %d name: %s", outputID, ev.Name) outputNames[outputID] = ev.Name isVirtual := len(ev.Name) >= 9 && ev.Name[:9] == "HEADLESS-" if isVirtual { log.Infof("Output %d identified as virtual", outputID) } }) if gammaMgr != nil { outputs = append(outputs, output) } m.outputRegNames.Store(outputID, e.Name) m.configMutex.RLock() enabled := m.config.Enabled m.configMutex.RUnlock() if enabled && m.controlsInitialized { m.post(func() { log.Infof("New output %d added, creating gamma control", outputID) if err := m.addOutputControl(output); err != nil { log.Errorf("Failed to add gamma control for new output %d: %v", outputID, err) } }) } else if enabled && !m.controlsInitialized { m.post(func() { log.Infof("Output %d added after all were removed, creating gamma control", outputID) if err := m.addOutputControl(output); err != nil { log.Errorf("Failed to add gamma control for output %d: %v", outputID, err) } else { m.controlsInitialized = true } }) } } else { log.Errorf("Failed to bind wl_output: %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 { log.Infof("Output %d (registry name %d) removed, destroying gamma control", foundID, e.Name) if foundOut.gammaControl != nil { control := foundOut.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) control.Destroy() } m.outputs.Delete(foundID) hasOutputs := false m.outputs.Range(func(key uint32, value *outputState) bool { hasOutputs = true return false }) if !hasOutputs { m.controlsInitialized = false log.Info("All outputs removed, controls no longer initialized") } } }) }) if err := m.display.Roundtrip(); err != nil { return fmt.Errorf("first roundtrip failed: %w", err) } if err := m.display.Roundtrip(); err != nil { return fmt.Errorf("second roundtrip failed: %w", err) } log.Infof("setupRegistry: discovered gamma_manager=%v, outputs=%d", gammaMgr != nil, len(outputs)) if gammaMgr == nil { log.Error("setupRegistry: gamma control manager not found in registry") return errdefs.ErrNoGammaControl } if len(outputs) == 0 { log.Error("setupRegistry: no wl_output objects found") return fmt.Errorf("no outputs available") } physicalOutputs := make([]*wlclient.Output, 0) for _, output := range outputs { outputID := output.ID() name := outputNames[outputID] if name != "" && (len(name) >= 9 && name[:9] == "HEADLESS-") { log.Infof("Skipping virtual output %d (name=%s) for gamma control", outputID, name) continue } physicalOutputs = append(physicalOutputs, output) } log.Infof("setupRegistry: filtered %d physical outputs from %d total outputs", len(physicalOutputs), len(outputs)) m.gammaControl = gammaMgr m.availableOutputs = physicalOutputs log.Info("setupRegistry: completed successfully (gamma controls will be initialized when enabled)") return nil } func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_gamma_control.ZwlrGammaControlManagerV1) error { log.Infof("setupOutputControls: creating gamma controls for %d outputs", len(outputs)) for _, output := range outputs { control, err := manager.GetGammaControl(output) if err != nil { log.Warnf("Failed to get gamma control for output %d: %v", output.ID(), err) continue } outputID := output.ID() registryName, _ := m.outputRegNames.Load(outputID) outState := &outputState{ id: outputID, registryName: registryName, output: output, gammaControl: control, isVirtual: false, } func(state *outputState) { control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { if outState, exists := m.outputs.Load(state.id); exists { outState.rampSize = e.Size outState.failed = false outState.retryCount = 0 log.Infof("Output %d gamma_size=%d", state.id, e.Size) } m.transitionMutex.RLock() currentTemp := m.currentTemp m.transitionMutex.RUnlock() m.post(func() { m.applyNowOnActor(currentTemp) }) }) control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { if outState, exists := m.outputs.Load(state.id); exists { outState.failed = true outState.rampSize = 0 outState.retryCount++ outState.lastFailTime = time.Now() retryCount := outState.retryCount if retryCount == 1 || retryCount%5 == 0 { log.Errorf("Gamma control failed for output %d (attempt %d)", state.id, retryCount) } backoff := time.Duration(300<= 9 && ev.Name[:9] == "HEADLESS-" { log.Infof("Detected virtual output %d (name=%s), marking for gamma control skip", outputID, ev.Name) outState.isVirtual = true outState.failed = true } } }) gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) control, err := gammaMgr.GetGammaControl(output) if err != nil { return fmt.Errorf("failed to get gamma control: %w", err) } registryName, _ := m.outputRegNames.Load(outputID) outState := &outputState{ id: outputID, name: outputName, registryName: registryName, output: output, gammaControl: control, isVirtual: false, } control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { if out, exists := m.outputs.Load(outState.id); exists { out.rampSize = e.Size out.failed = false out.retryCount = 0 log.Infof("Output %d gamma_size=%d", outState.id, e.Size) } m.transitionMutex.RLock() currentTemp := m.currentTemp m.transitionMutex.RUnlock() m.post(func() { m.applyNowOnActor(currentTemp) }) }) control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { if out, exists := m.outputs.Load(outState.id); exists { out.failed = true out.rampSize = 0 out.retryCount++ out.lastFailTime = time.Now() retryCount := out.retryCount if retryCount == 1 || retryCount%5 == 0 { log.Errorf("Gamma control failed for output %d (attempt %d)", outState.id, retryCount) } backoff := time.Duration(300< %dK over %v", currentTemp, targetTemp, dur) for i := 0; i <= steps; i++ { select { case newTarget := <-m.transitionChan: m.transitionMutex.Lock() m.targetTemp = newTarget m.transitionMutex.Unlock() log.Debugf("Transition %dK -> %dK aborted (newer transition started)", currentTemp, targetTemp) break default: } m.transitionMutex.RLock() if m.targetTemp != targetTemp { m.transitionMutex.RUnlock() break } m.transitionMutex.RUnlock() progress := float64(i) / float64(steps) temp := currentTemp + int(float64(targetTemp-currentTemp)*progress) m.post(func() { m.applyNowOnActor(temp) }) if i < steps { time.Sleep(stepDur) } } m.transitionMutex.RLock() finalTarget := m.targetTemp m.transitionMutex.RUnlock() if finalTarget == targetTemp { log.Debugf("Transition complete: now at %dK", targetTemp) m.configMutex.RLock() enabled := m.config.Enabled identityTemp := m.config.HighTemp m.configMutex.RUnlock() if !enabled && targetTemp == identityTemp && m.controlsInitialized { m.post(func() { log.Info("Destroying gamma controls after transition to identity") m.outputs.Range(func(id uint32, out *outputState) bool { if out.gammaControl != nil { control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) control.Destroy() log.Debugf("Destroyed gamma control for output %d", id) } return true }) m.outputs.Range(func(key uint32, value *outputState) bool { m.outputs.Delete(key) return true }) m.controlsInitialized = false m.transitionMutex.Lock() m.currentTemp = identityTemp m.targetTemp = identityTemp m.transitionMutex.Unlock() if _, err := m.display.Sync(); err != nil { log.Warnf("Failed to sync Wayland display after destroying controls: %v", err) } log.Info("All gamma controls destroyed") }) } } } } } func (m *Manager) recreateOutputControl(out *outputState) error { m.configMutex.RLock() enabled := m.config.Enabled m.configMutex.RUnlock() if !enabled || !m.controlsInitialized { return nil } _, exists := m.outputs.Load(out.id) if !exists { return nil } if out.isVirtual { return nil } const maxRetries = 10 if out.retryCount >= maxRetries { return nil } gammaMgr, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) if !ok || gammaMgr == nil { return fmt.Errorf("gamma control manager not available") } control, err := gammaMgr.GetGammaControl(out.output) if err != nil { return fmt.Errorf("get gamma control: %w", err) } state := out control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { if outState, exists := m.outputs.Load(state.id); exists { outState.rampSize = e.Size outState.failed = false outState.retryCount = 0 log.Infof("Output %d gamma_size=%d (recreated)", state.id, e.Size) } m.transitionMutex.RLock() currentTemp := m.currentTemp m.transitionMutex.RUnlock() m.post(func() { m.applyNowOnActor(currentTemp) }) }) control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { if outState, exists := m.outputs.Load(state.id); exists { outState.failed = true outState.rampSize = 0 outState.retryCount++ outState.lastFailTime = time.Now() retryCount := outState.retryCount if retryCount == 1 || retryCount%5 == 0 { log.Errorf("Gamma control failed for output %d (attempt %d)", state.id, retryCount) } backoff := time.Duration(300<