summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/handlers.go282
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager.go492
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager_test.go414
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/types.go192
4 files changed, 1380 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/handlers.go
new file mode 100644
index 0000000..c885e13
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/handlers.go
@@ -0,0 +1,282 @@
+package wlroutput
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+ "time"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
+)
+
+type HeadConfig struct {
+ Name string `json:"name"`
+ Enabled bool `json:"enabled"`
+ ModeID *uint32 `json:"modeId,omitempty"`
+ CustomMode *struct {
+ Width int32 `json:"width"`
+ Height int32 `json:"height"`
+ Refresh int32 `json:"refresh"`
+ } `json:"customMode,omitempty"`
+ Position *struct{ X, Y int32 } `json:"position,omitempty"`
+ Transform *int32 `json:"transform,omitempty"`
+ Scale *float64 `json:"scale,omitempty"`
+ AdaptiveSync *uint32 `json:"adaptiveSync,omitempty"`
+}
+
+type ConfigurationRequest struct {
+ Heads []HeadConfig `json:"heads"`
+ Test bool `json:"test"`
+}
+
+func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
+ if manager == nil {
+ models.RespondError(conn, req.ID, "wlroutput manager not initialized")
+ return
+ }
+
+ switch req.Method {
+ case "wlroutput.getState":
+ handleGetState(conn, req, manager)
+ case "wlroutput.applyConfiguration":
+ handleApplyConfiguration(conn, req, manager, false)
+ case "wlroutput.testConfiguration":
+ handleApplyConfiguration(conn, req, manager, true)
+ case "wlroutput.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 handleApplyConfiguration(conn net.Conn, req models.Request, manager *Manager, test bool) {
+ headsParam, ok := models.Get[any](req, "heads")
+ if !ok {
+ models.RespondError(conn, req.ID, "missing 'heads' parameter")
+ return
+ }
+
+ headsJSON, err := json.Marshal(headsParam)
+ if err != nil {
+ models.RespondError(conn, req.ID, "invalid 'heads' parameter format")
+ return
+ }
+
+ var heads []HeadConfig
+ if err := json.Unmarshal(headsJSON, &heads); err != nil {
+ models.RespondError(conn, req.ID, fmt.Sprintf("invalid heads configuration: %v", err))
+ return
+ }
+
+ if err := manager.ApplyConfiguration(heads, test); err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ msg := "configuration applied"
+ if test {
+ msg = "configuration test succeeded"
+ }
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: msg})
+}
+
+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
+ }
+ }
+}
+
+func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
+ if m.manager == nil {
+ return fmt.Errorf("output manager not initialized")
+ }
+
+ resultChan := make(chan error, 1)
+
+ m.post(func() {
+ m.wlMutex.Lock()
+ defer m.wlMutex.Unlock()
+
+ config, err := m.manager.CreateConfiguration(m.serial)
+ if err != nil {
+ resultChan <- fmt.Errorf("failed to create configuration: %w", err)
+ return
+ }
+
+ responded := false
+
+ config.SetSucceededHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1SucceededEvent) {
+ if responded {
+ return
+ }
+ responded = true
+ log.Info("WlrOutput: configuration succeeded")
+ config.Destroy()
+ resultChan <- nil
+ })
+
+ config.SetFailedHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1FailedEvent) {
+ if responded {
+ return
+ }
+ responded = true
+ log.Warn("WlrOutput: configuration failed")
+ config.Destroy()
+ resultChan <- fmt.Errorf("compositor rejected configuration")
+ })
+
+ config.SetCancelledHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1CancelledEvent) {
+ if responded {
+ return
+ }
+ responded = true
+ log.Warn("WlrOutput: configuration cancelled")
+ config.Destroy()
+ resultChan <- fmt.Errorf("configuration cancelled (outdated serial)")
+ })
+
+ time.AfterFunc(time.Second, func() {
+ m.post(func() {
+ if responded {
+ return
+ }
+ responded = true
+ config.Destroy()
+ resultChan <- fmt.Errorf("timeout waiting for configuration response")
+ })
+ })
+
+ headsByName := make(map[string]*headState)
+ m.heads.Range(func(key uint32, head *headState) bool {
+ if !head.finished {
+ headsByName[head.name] = head
+ }
+ return true
+ })
+
+ for _, headCfg := range heads {
+ head, exists := headsByName[headCfg.Name]
+ if !exists {
+ config.Destroy()
+ resultChan <- fmt.Errorf("head not found: %s", headCfg.Name)
+ return
+ }
+
+ if !headCfg.Enabled {
+ if err := config.DisableHead(head.handle); err != nil {
+ config.Destroy()
+ resultChan <- fmt.Errorf("failed to disable head %s: %w", headCfg.Name, err)
+ return
+ }
+ continue
+ }
+
+ headConfig, err := config.EnableHead(head.handle)
+ if err != nil {
+ config.Destroy()
+ resultChan <- fmt.Errorf("failed to enable head %s: %w", headCfg.Name, err)
+ return
+ }
+
+ if headCfg.ModeID != nil {
+ mode, exists := m.modes.Load(*headCfg.ModeID)
+
+ if !exists {
+ config.Destroy()
+ resultChan <- fmt.Errorf("mode not found: %d", *headCfg.ModeID)
+ return
+ }
+
+ if err := headConfig.SetMode(mode.handle); err != nil {
+ config.Destroy()
+ resultChan <- fmt.Errorf("failed to set mode for %s: %w", headCfg.Name, err)
+ return
+ }
+ } else if headCfg.CustomMode != nil {
+ if err := headConfig.SetCustomMode(
+ headCfg.CustomMode.Width,
+ headCfg.CustomMode.Height,
+ headCfg.CustomMode.Refresh,
+ ); err != nil {
+ config.Destroy()
+ resultChan <- fmt.Errorf("failed to set custom mode for %s: %w", headCfg.Name, err)
+ return
+ }
+ }
+
+ if headCfg.Position != nil {
+ if err := headConfig.SetPosition(headCfg.Position.X, headCfg.Position.Y); err != nil {
+ config.Destroy()
+ resultChan <- fmt.Errorf("failed to set position for %s: %w", headCfg.Name, err)
+ return
+ }
+ }
+
+ if headCfg.Transform != nil {
+ if err := headConfig.SetTransform(*headCfg.Transform); err != nil {
+ config.Destroy()
+ resultChan <- fmt.Errorf("failed to set transform for %s: %w", headCfg.Name, err)
+ return
+ }
+ }
+
+ if headCfg.Scale != nil {
+ if err := headConfig.SetScale(*headCfg.Scale); err != nil {
+ config.Destroy()
+ resultChan <- fmt.Errorf("failed to set scale for %s: %w", headCfg.Name, err)
+ return
+ }
+ }
+
+ if headCfg.AdaptiveSync != nil {
+ if err := headConfig.SetAdaptiveSync(*headCfg.AdaptiveSync); err != nil {
+ config.Destroy()
+ resultChan <- fmt.Errorf("failed to set adaptive sync for %s: %w", headCfg.Name, err)
+ return
+ }
+ }
+ }
+
+ var applyErr error
+ if test {
+ applyErr = config.Test()
+ } else {
+ applyErr = config.Apply()
+ }
+
+ if applyErr != nil {
+ responded = true
+ config.Destroy()
+ action := "apply"
+ if test {
+ action = "test"
+ }
+ resultChan <- fmt.Errorf("failed to %s configuration: %w", action, applyErr)
+ return
+ }
+ })
+
+ return <-resultChan
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager.go
new file mode 100644
index 0000000..cbd89a3
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager.go
@@ -0,0 +1,492 @@
+package wlroutput
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
+ wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
+)
+
+func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
+ m := &Manager{
+ display: display,
+ ctx: display.Context(),
+ cmdq: make(chan cmd, 512),
+ stopChan: make(chan struct{}),
+ dirty: make(chan struct{}, 1),
+ fatalError: make(chan error, 1),
+ }
+
+ m.wg.Add(1)
+ go m.waylandActor()
+
+ if err := m.setupRegistry(); err != nil {
+ close(m.stopChan)
+ m.wg.Wait()
+ return nil, err
+ }
+
+ m.updateState()
+
+ m.notifierWg.Add(1)
+ go m.notifier()
+
+ return m, nil
+}
+
+func (m *Manager) post(fn func()) {
+ select {
+ case m.cmdq <- cmd{fn: fn}:
+ default:
+ log.Warn("WlrOutput actor command queue full, dropping command")
+ }
+}
+
+func (m *Manager) waylandActor() {
+ defer m.wg.Done()
+ defer func() {
+ if r := recover(); r != nil {
+ err := fmt.Errorf("waylandActor panic: %v", r)
+ log.Errorf("WlrOutput: %v", err)
+
+ select {
+ case m.fatalError <- err:
+ default:
+ }
+
+ select {
+ case <-m.stopChan:
+ default:
+ close(m.stopChan)
+ }
+ }
+ }()
+
+ for {
+ select {
+ case <-m.stopChan:
+ return
+ case c := <-m.cmdq:
+ func() {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Errorf("WlrOutput: command execution panic: %v", r)
+ }
+ }()
+ c.fn()
+ }()
+ }
+ }
+}
+
+func (m *Manager) setupRegistry() error {
+ log.Info("WlrOutput: starting registry setup")
+
+ registry, err := m.display.GetRegistry()
+ if err != nil {
+ return fmt.Errorf("failed to get registry: %w", err)
+ }
+ m.registry = registry
+
+ registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
+ if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
+ log.Infof("WlrOutput: found %s", wlr_output_management.ZwlrOutputManagerV1InterfaceName)
+ manager := wlr_output_management.NewZwlrOutputManagerV1(m.ctx)
+ version := e.Version
+ if version > 4 {
+ version = 4
+ }
+
+ manager.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
+ m.handleHead(e)
+ })
+
+ manager.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
+ log.Debugf("WlrOutput: done event received (serial=%d)", e.Serial)
+ m.serial = e.Serial
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ manager.SetFinishedHandler(func(e wlr_output_management.ZwlrOutputManagerV1FinishedEvent) {
+ log.Info("WlrOutput: finished event received")
+ })
+
+ if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil {
+ m.manager = manager
+ log.Info("WlrOutput: manager bound successfully")
+ } else {
+ log.Errorf("WlrOutput: failed to bind manager: %v", err)
+ }
+ }
+ })
+
+ log.Info("WlrOutput: registry setup complete (events will be processed async)")
+ return nil
+}
+
+func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
+ handle := e.Head
+ headID := handle.ID()
+
+ log.Debugf("WlrOutput: New head (id=%d)", headID)
+
+ head := &headState{
+ id: headID,
+ handle: handle,
+ modeIDs: make([]uint32, 0),
+ }
+
+ m.heads.Store(headID, head)
+
+ handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
+ log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name)
+ head.name = e.Name
+ head.ready = true
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetDescriptionHandler(func(e wlr_output_management.ZwlrOutputHeadV1DescriptionEvent) {
+ log.Debugf("WlrOutput: Head %d description: %s", headID, e.Description)
+ head.description = e.Description
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetPhysicalSizeHandler(func(e wlr_output_management.ZwlrOutputHeadV1PhysicalSizeEvent) {
+ log.Debugf("WlrOutput: Head %d physical size: %dx%d", headID, e.Width, e.Height)
+ head.physicalWidth = e.Width
+ head.physicalHeight = e.Height
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) {
+ m.handleMode(headID, e)
+ })
+
+ handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) {
+ log.Debugf("WlrOutput: Head %d enabled: %d", headID, e.Enabled)
+ head.enabled = e.Enabled != 0
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) {
+ modeID := e.Mode.ID()
+ log.Debugf("WlrOutput: Head %d current mode: %d", headID, modeID)
+ head.currentModeID = modeID
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetPositionHandler(func(e wlr_output_management.ZwlrOutputHeadV1PositionEvent) {
+ log.Debugf("WlrOutput: Head %d position: %d,%d", headID, e.X, e.Y)
+ head.x = e.X
+ head.y = e.Y
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetTransformHandler(func(e wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
+ log.Debugf("WlrOutput: Head %d transform: %d", headID, e.Transform)
+ head.transform = e.Transform
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
+ log.Debugf("WlrOutput: Head %d scale: %f", headID, e.Scale)
+ head.scale = e.Scale
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetMakeHandler(func(e wlr_output_management.ZwlrOutputHeadV1MakeEvent) {
+ log.Debugf("WlrOutput: Head %d make: %s", headID, e.Make)
+ head.make = e.Make
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetModelHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModelEvent) {
+ log.Debugf("WlrOutput: Head %d model: %s", headID, e.Model)
+ head.model = e.Model
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetSerialNumberHandler(func(e wlr_output_management.ZwlrOutputHeadV1SerialNumberEvent) {
+ log.Debugf("WlrOutput: Head %d serial: %s", headID, e.SerialNumber)
+ head.serialNumber = e.SerialNumber
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetAdaptiveSyncHandler(func(e wlr_output_management.ZwlrOutputHeadV1AdaptiveSyncEvent) {
+ log.Debugf("WlrOutput: Head %d adaptive sync: %d", headID, e.State)
+ head.adaptiveSync = e.State
+ head.adaptiveSyncSupported = true
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetFinishedHandler(func(e wlr_output_management.ZwlrOutputHeadV1FinishedEvent) {
+ log.Debugf("WlrOutput: Head %d finished", headID)
+ head.finished = true
+
+ m.heads.Delete(headID)
+
+ m.wlMutex.Lock()
+ handle.Release()
+ m.wlMutex.Unlock()
+
+ m.post(func() {
+ m.updateState()
+ })
+ })
+}
+
+func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHeadV1ModeEvent) {
+ handle := e.Mode
+ modeID := handle.ID()
+
+ log.Debugf("WlrOutput: Head %d new mode (id=%d)", headID, modeID)
+
+ mode := &modeState{
+ id: modeID,
+ handle: handle,
+ }
+
+ m.modes.Store(modeID, mode)
+
+ if head, ok := m.heads.Load(headID); ok {
+ head.modeIDs = append(head.modeIDs, modeID)
+ m.heads.Store(headID, head)
+ }
+
+ handle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
+ log.Debugf("WlrOutput: Mode %d size: %dx%d", modeID, e.Width, e.Height)
+ mode.width = e.Width
+ mode.height = e.Height
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) {
+ log.Debugf("WlrOutput: Mode %d refresh: %d", modeID, e.Refresh)
+ mode.refresh = e.Refresh
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetPreferredHandler(func(e wlr_output_management.ZwlrOutputModeV1PreferredEvent) {
+ log.Debugf("WlrOutput: Mode %d preferred", modeID)
+ mode.preferred = true
+ m.post(func() {
+ m.updateState()
+ })
+ })
+
+ handle.SetFinishedHandler(func(e wlr_output_management.ZwlrOutputModeV1FinishedEvent) {
+ log.Debugf("WlrOutput: Mode %d finished", modeID)
+ mode.finished = true
+
+ m.modes.Delete(modeID)
+
+ m.wlMutex.Lock()
+ handle.Release()
+ m.wlMutex.Unlock()
+
+ m.post(func() {
+ m.updateState()
+ })
+ })
+}
+
+func (m *Manager) updateState() {
+ outputs := make([]Output, 0)
+
+ m.heads.Range(func(key uint32, head *headState) bool {
+ if head.finished {
+ return true
+ }
+
+ if !head.ready {
+ return true
+ }
+
+ modes := make([]OutputMode, 0)
+ var currentMode *OutputMode
+
+ for _, modeID := range head.modeIDs {
+ mode, exists := m.modes.Load(modeID)
+ if !exists {
+ continue
+ }
+ if mode.finished {
+ continue
+ }
+
+ outMode := OutputMode{
+ Width: mode.width,
+ Height: mode.height,
+ Refresh: mode.refresh,
+ Preferred: mode.preferred,
+ ID: modeID,
+ }
+ modes = append(modes, outMode)
+
+ if modeID == head.currentModeID {
+ currentMode = &outMode
+ }
+ }
+
+ output := Output{
+ Name: head.name,
+ Description: head.description,
+ Make: head.make,
+ Model: head.model,
+ SerialNumber: head.serialNumber,
+ PhysicalWidth: head.physicalWidth,
+ PhysicalHeight: head.physicalHeight,
+ Enabled: head.enabled,
+ X: head.x,
+ Y: head.y,
+ Transform: head.transform,
+ Scale: head.scale,
+ CurrentMode: currentMode,
+ Modes: modes,
+ AdaptiveSync: head.adaptiveSync,
+ AdaptiveSyncSupported: head.adaptiveSyncSupported,
+ ID: head.id,
+ }
+ outputs = append(outputs, output)
+ return true
+ })
+
+ newState := State{
+ Outputs: outputs,
+ Serial: m.serial,
+ }
+
+ m.stateMutex.Lock()
+ m.state = &newState
+ m.stateMutex.Unlock()
+
+ m.notifySubscribers()
+}
+
+func (m *Manager) notifier() {
+ defer m.notifierWg.Done()
+ defer func() {
+ if r := recover(); r != nil {
+ err := fmt.Errorf("notifier panic: %v", r)
+ log.Errorf("WlrOutput: %v", err)
+
+ select {
+ case m.fatalError <- err:
+ default:
+ }
+
+ select {
+ case <-m.stopChan:
+ default:
+ close(m.stopChan)
+ }
+ }
+ }()
+
+ 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, &currentState) {
+ pending = false
+ continue
+ }
+
+ m.subscribers.Range(func(key string, ch chan State) bool {
+ select {
+ case ch <- currentState:
+ default:
+ log.Warn("WlrOutput: subscriber channel full, dropping update")
+ }
+ return true
+ })
+
+ stateCopy := currentState
+ m.lastNotified = &stateCopy
+ pending = false
+ }
+ }
+}
+
+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.modes.Range(func(key uint32, mode *modeState) bool {
+ if mode.handle != nil {
+ mode.handle.Release()
+ }
+ m.modes.Delete(key)
+ return true
+ })
+
+ m.heads.Range(func(key uint32, head *headState) bool {
+ if head.handle != nil {
+ head.handle.Release()
+ }
+ m.heads.Delete(key)
+ return true
+ })
+
+ if m.manager != nil {
+ m.manager.Stop()
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager_test.go
new file mode 100644
index 0000000..e90c70c
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/manager_test.go
@@ -0,0 +1,414 @@
+package wlroutput
+
+import (
+ "errors"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+
+ mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
+)
+
+func TestStateChanged_BothNil(t *testing.T) {
+ assert.True(t, stateChanged(nil, nil))
+}
+
+func TestStateChanged_OneNil(t *testing.T) {
+ s := &State{Serial: 1}
+ assert.True(t, stateChanged(s, nil))
+ assert.True(t, stateChanged(nil, s))
+}
+
+func TestStateChanged_SerialDiffers(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{}}
+ b := &State{Serial: 2, Outputs: []Output{}}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_OutputCountDiffers(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}}}
+ b := &State{Serial: 1, Outputs: []Output{}}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_OutputNameDiffers(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Enabled: true}}}
+ b := &State{Serial: 1, Outputs: []Output{{Name: "HDMI-A-1", Enabled: true}}}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_OutputEnabledDiffers(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Enabled: true}}}
+ b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Enabled: false}}}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_OutputPositionDiffers(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", X: 0, Y: 0}}}
+ b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", X: 1920, Y: 0}}}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_OutputTransformDiffers(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Transform: 0}}}
+ b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Transform: 1}}}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_OutputScaleDiffers(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Scale: 1.0}}}
+ b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Scale: 2.0}}}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_OutputAdaptiveSyncDiffers(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", AdaptiveSync: 0}}}
+ b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", AdaptiveSync: 1}}}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_CurrentModeNilVsNonNil(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: nil}}}
+ b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: &OutputMode{Width: 1920}}}}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_CurrentModeDiffers(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{{
+ Name: "eDP-1",
+ CurrentMode: &OutputMode{Width: 1920, Height: 1080, Refresh: 60000},
+ }}}
+ b := &State{Serial: 1, Outputs: []Output{{
+ Name: "eDP-1",
+ CurrentMode: &OutputMode{Width: 2560, Height: 1440, Refresh: 60000},
+ }}}
+ assert.True(t, stateChanged(a, b))
+
+ b.Outputs[0].CurrentMode.Width = 1920
+ b.Outputs[0].CurrentMode.Height = 1080
+ b.Outputs[0].CurrentMode.Refresh = 144000
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_ModesLengthDiffers(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Modes: []OutputMode{{Width: 1920}}}}}
+ b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", Modes: []OutputMode{{Width: 1920}, {Width: 1280}}}}}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_Equal(t *testing.T) {
+ mode := OutputMode{Width: 1920, Height: 1080, Refresh: 60000, Preferred: true}
+ a := &State{
+ Serial: 5,
+ Outputs: []Output{{
+ Name: "eDP-1",
+ Description: "Built-in display",
+ Make: "BOE",
+ Model: "0x0ABC",
+ SerialNumber: "12345",
+ PhysicalWidth: 309,
+ PhysicalHeight: 174,
+ Enabled: true,
+ X: 0,
+ Y: 0,
+ Transform: 0,
+ Scale: 1.0,
+ CurrentMode: &mode,
+ Modes: []OutputMode{mode},
+ AdaptiveSync: 0,
+ }},
+ }
+ b := &State{
+ Serial: 5,
+ Outputs: []Output{{
+ Name: "eDP-1",
+ Description: "Built-in display",
+ Make: "BOE",
+ Model: "0x0ABC",
+ SerialNumber: "12345",
+ PhysicalWidth: 309,
+ PhysicalHeight: 174,
+ Enabled: true,
+ X: 0,
+ Y: 0,
+ Transform: 0,
+ Scale: 1.0,
+ CurrentMode: &mode,
+ Modes: []OutputMode{mode},
+ AdaptiveSync: 0,
+ }},
+ }
+ assert.False(t, stateChanged(a, b))
+}
+
+func TestManager_ConcurrentGetState(t *testing.T) {
+ m := &Manager{
+ state: &State{
+ Serial: 1,
+ Outputs: []Output{{Name: "eDP-1", Enabled: 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()
+ _ = s.Serial
+ _ = s.Outputs
+ }
+ }()
+ }
+
+ 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{
+ Serial: uint32(j),
+ Outputs: []Output{{Name: "eDP-1", Scale: float64(j % 3)}},
+ }
+ m.stateMutex.Unlock()
+ }
+ }(i)
+ }
+
+ wg.Wait()
+}
+
+func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
+ m := &Manager{
+ stopChan: make(chan struct{}),
+ dirty: 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_SyncmapHeadsConcurrentAccess(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 := &headState{
+ id: key,
+ name: "test-head",
+ enabled: j%2 == 0,
+ scale: float64(j % 3),
+ modeIDs: []uint32{uint32(j)},
+ }
+ m.heads.Store(key, state)
+
+ if loaded, ok := m.heads.Load(key); ok {
+ assert.Equal(t, key, loaded.id)
+ }
+
+ m.heads.Range(func(k uint32, v *headState) bool {
+ _ = v.name
+ _ = v.enabled
+ return true
+ })
+ }
+
+ m.heads.Delete(key)
+ }(i)
+ }
+
+ wg.Wait()
+}
+
+func TestManager_SyncmapModesConcurrentAccess(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 := &modeState{
+ id: key,
+ width: int32(1920 + j),
+ height: int32(1080 + j),
+ refresh: 60000,
+ preferred: j == 0,
+ }
+ m.modes.Store(key, state)
+
+ if loaded, ok := m.modes.Load(key); ok {
+ assert.Equal(t, key, loaded.id)
+ }
+
+ m.modes.Range(func(k uint32, v *modeState) bool {
+ _ = v.width
+ _ = v.height
+ return true
+ })
+ }
+
+ m.modes.Delete(key)
+ }(i)
+ }
+
+ wg.Wait()
+}
+
+func TestManager_NotifySubscribersNonBlocking(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 TestManager_PostQueueFull(t *testing.T) {
+ m := &Manager{
+ cmdq: make(chan cmd, 2),
+ stopChan: make(chan struct{}),
+ }
+
+ m.post(func() {})
+ m.post(func() {})
+ m.post(func() {})
+ m.post(func() {})
+
+ assert.Len(t, m.cmdq, 2)
+}
+
+func TestManager_GetStateNilState(t *testing.T) {
+ m := &Manager{}
+
+ s := m.GetState()
+ assert.NotNil(t, s.Outputs)
+ assert.Equal(t, uint32(0), s.Serial)
+}
+
+func TestManager_FatalErrorChannel(t *testing.T) {
+ m := &Manager{
+ fatalError: make(chan error, 1),
+ }
+
+ ch := m.FatalError()
+ assert.NotNil(t, ch)
+
+ m.fatalError <- assert.AnError
+ err := <-ch
+ assert.Error(t, err)
+}
+
+func TestOutputMode_Fields(t *testing.T) {
+ mode := OutputMode{
+ Width: 1920,
+ Height: 1080,
+ Refresh: 60000,
+ Preferred: true,
+ ID: 42,
+ }
+
+ assert.Equal(t, int32(1920), mode.Width)
+ assert.Equal(t, int32(1080), mode.Height)
+ assert.Equal(t, int32(60000), mode.Refresh)
+ assert.True(t, mode.Preferred)
+ assert.Equal(t, uint32(42), mode.ID)
+}
+
+func TestOutput_Fields(t *testing.T) {
+ out := Output{
+ Name: "eDP-1",
+ Description: "Built-in display",
+ Make: "BOE",
+ Model: "0x0ABC",
+ SerialNumber: "12345",
+ PhysicalWidth: 309,
+ PhysicalHeight: 174,
+ Enabled: true,
+ X: 0,
+ Y: 0,
+ Transform: 0,
+ Scale: 1.5,
+ AdaptiveSync: 1,
+ ID: 1,
+ }
+
+ assert.Equal(t, "eDP-1", out.Name)
+ assert.Equal(t, "Built-in display", out.Description)
+ assert.True(t, out.Enabled)
+ assert.Equal(t, float64(1.5), out.Scale)
+ assert.Equal(t, uint32(1), out.AdaptiveSync)
+}
+
+func TestHeadState_ModeIDsSlice(t *testing.T) {
+ head := &headState{
+ id: 1,
+ modeIDs: make([]uint32, 0),
+ }
+
+ head.modeIDs = append(head.modeIDs, 1, 2, 3)
+ assert.Len(t, head.modeIDs, 3)
+ assert.Equal(t, uint32(1), head.modeIDs[0])
+}
+
+func TestStateChanged_BothCurrentModeNil(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: nil}}}
+ b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1", CurrentMode: nil}}}
+ assert.False(t, stateChanged(a, b))
+}
+
+func TestStateChanged_IndexOutOfBounds(t *testing.T) {
+ a := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}}}
+ b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}, {Name: "HDMI-A-1"}}}
+ assert.True(t, stateChanged(a, b))
+}
+
+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"))
+
+ _, err := NewManager(mockDisplay)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "failed to get registry")
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/types.go
new file mode 100644
index 0000000..9c79b1a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/wlroutput/types.go
@@ -0,0 +1,192 @@
+package wlroutput
+
+import (
+ "sync"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
+ wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
+ "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
+)
+
+type OutputMode struct {
+ Width int32 `json:"width"`
+ Height int32 `json:"height"`
+ Refresh int32 `json:"refresh"`
+ Preferred bool `json:"preferred"`
+ ID uint32 `json:"id"`
+}
+
+type Output struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Make string `json:"make"`
+ Model string `json:"model"`
+ SerialNumber string `json:"serialNumber"`
+ PhysicalWidth int32 `json:"physicalWidth"`
+ PhysicalHeight int32 `json:"physicalHeight"`
+ Enabled bool `json:"enabled"`
+ X int32 `json:"x"`
+ Y int32 `json:"y"`
+ Transform int32 `json:"transform"`
+ Scale float64 `json:"scale"`
+ CurrentMode *OutputMode `json:"currentMode"`
+ Modes []OutputMode `json:"modes"`
+ AdaptiveSync uint32 `json:"adaptiveSync"`
+ AdaptiveSyncSupported bool `json:"adaptiveSyncSupported"`
+ ID uint32 `json:"id"`
+}
+
+type State struct {
+ Outputs []Output `json:"outputs"`
+ Serial uint32 `json:"serial"`
+}
+
+type cmd struct {
+ fn func()
+}
+
+type Manager struct {
+ display wlclient.WaylandDisplay
+ ctx *wlclient.Context
+ registry *wlclient.Registry
+ manager *wlr_output_management.ZwlrOutputManagerV1
+
+ heads syncmap.Map[uint32, *headState]
+ modes syncmap.Map[uint32, *modeState]
+
+ serial uint32
+
+ wlMutex sync.Mutex
+ cmdq chan cmd
+ stopChan chan struct{}
+ wg sync.WaitGroup
+
+ subscribers syncmap.Map[string, chan State]
+ dirty chan struct{}
+ notifierWg sync.WaitGroup
+ lastNotified *State
+
+ stateMutex sync.RWMutex
+ state *State
+
+ fatalError chan error
+}
+
+type headState struct {
+ id uint32
+ handle *wlr_output_management.ZwlrOutputHeadV1
+ name string
+ description string
+ make string
+ model string
+ serialNumber string
+ physicalWidth int32
+ physicalHeight int32
+ enabled bool
+ x int32
+ y int32
+ transform int32
+ scale float64
+ currentModeID uint32
+ modeIDs []uint32
+ adaptiveSync uint32
+ adaptiveSyncSupported bool
+ finished bool
+ ready bool
+}
+
+type modeState struct {
+ id uint32
+ handle *wlr_output_management.ZwlrOutputModeV1
+ width int32
+ height int32
+ refresh int32
+ preferred bool
+ finished bool
+}
+
+func (m *Manager) GetState() State {
+ m.stateMutex.RLock()
+ defer m.stateMutex.RUnlock()
+ if m.state == nil {
+ return State{
+ Outputs: []Output{},
+ Serial: 0,
+ }
+ }
+ stateCopy := *m.state
+ return stateCopy
+}
+
+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 (m *Manager) FatalError() <-chan error {
+ return m.fatalError
+}
+
+func stateChanged(old, new *State) bool {
+ if old == nil || new == nil {
+ return true
+ }
+ if old.Serial != new.Serial {
+ return true
+ }
+ if len(old.Outputs) != len(new.Outputs) {
+ return true
+ }
+ for i := range new.Outputs {
+ if i >= len(old.Outputs) {
+ return true
+ }
+ oldOut := &old.Outputs[i]
+ newOut := &new.Outputs[i]
+ if oldOut.Name != newOut.Name || oldOut.Enabled != newOut.Enabled {
+ return true
+ }
+ if oldOut.X != newOut.X || oldOut.Y != newOut.Y {
+ return true
+ }
+ if oldOut.Transform != newOut.Transform || oldOut.Scale != newOut.Scale {
+ return true
+ }
+ if oldOut.AdaptiveSync != newOut.AdaptiveSync || oldOut.AdaptiveSyncSupported != newOut.AdaptiveSyncSupported {
+ return true
+ }
+ if (oldOut.CurrentMode == nil) != (newOut.CurrentMode == nil) {
+ return true
+ }
+ if oldOut.CurrentMode != nil && newOut.CurrentMode != nil {
+ if oldOut.CurrentMode.Width != newOut.CurrentMode.Width ||
+ oldOut.CurrentMode.Height != newOut.CurrentMode.Height ||
+ oldOut.CurrentMode.Refresh != newOut.CurrentMode.Refresh {
+ return true
+ }
+ }
+ if len(oldOut.Modes) != len(newOut.Modes) {
+ return true
+ }
+ }
+ return false
+}