1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-13 00:42:49 -05:00

rename backend to core

This commit is contained in:
bbedward
2025-11-12 23:12:31 -05:00
parent 0fdc0748cf
commit db584b7897
280 changed files with 265 additions and 265 deletions

View File

@@ -0,0 +1,281 @@
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 Request struct {
ID int `json:"id,omitempty"`
Method string `json:"method"`
Params map[string]interface{} `json:"params,omitempty"`
}
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
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 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 Request, manager *Manager) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
}
func handleApplyConfiguration(conn net.Conn, req Request, manager *Manager, test bool) {
headsParam, ok := req.Params["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, SuccessResult{Success: true, Message: msg})
}
func handleSubscribe(conn net.Conn, req 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
}
statusChan := make(chan error, 1)
config.SetSucceededHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1SucceededEvent) {
log.Info("WlrOutput: configuration succeeded")
statusChan <- nil
})
config.SetFailedHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1FailedEvent) {
log.Warn("WlrOutput: configuration failed")
statusChan <- fmt.Errorf("compositor rejected configuration")
})
config.SetCancelledHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1CancelledEvent) {
log.Warn("WlrOutput: configuration cancelled")
statusChan <- fmt.Errorf("configuration cancelled (outdated serial)")
})
m.headsMutex.RLock()
headsByName := make(map[string]*headState)
for _, head := range m.heads {
if !head.finished {
headsByName[head.name] = head
}
}
m.headsMutex.RUnlock()
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 {
m.modesMutex.RLock()
mode, exists := m.modes[*headCfg.ModeID]
m.modesMutex.RUnlock()
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 {
config.Destroy()
action := "apply"
if test {
action = "test"
}
resultChan <- fmt.Errorf("failed to %s configuration: %w", action, applyErr)
return
}
go func() {
select {
case err := <-statusChan:
config.Destroy()
resultChan <- err
case <-time.After(5 * time.Second):
config.Destroy()
resultChan <- fmt.Errorf("timeout waiting for configuration response")
}
}()
})
return <-resultChan
}

View File

@@ -0,0 +1,511 @@
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/yaslama/go-wayland/wayland/client"
)
func NewManager(display *wlclient.Display) (*Manager, error) {
m := &Manager{
display: display,
heads: make(map[uint32]*headState),
modes: make(map[uint32]*modeState),
cmdq: make(chan cmd, 128),
stopChan: make(chan struct{}),
subscribers: make(map[string]chan State),
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")
ctx := m.display.Context()
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(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.headsMutex.Lock()
m.heads[headID] = head
m.headsMutex.Unlock()
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name)
head.name = e.Name
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
m.post(func() {
m.updateState()
})
})
handle.SetFinishedHandler(func(e wlr_output_management.ZwlrOutputHeadV1FinishedEvent) {
log.Debugf("WlrOutput: Head %d finished", headID)
head.finished = true
m.headsMutex.Lock()
delete(m.heads, headID)
m.headsMutex.Unlock()
m.post(func() {
m.wlMutex.Lock()
handle.Release()
m.wlMutex.Unlock()
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.modesMutex.Lock()
m.modes[modeID] = mode
m.modesMutex.Unlock()
m.headsMutex.Lock()
if head, ok := m.heads[headID]; ok {
head.modeIDs = append(head.modeIDs, modeID)
}
m.headsMutex.Unlock()
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.modesMutex.Lock()
delete(m.modes, modeID)
m.modesMutex.Unlock()
m.post(func() {
m.wlMutex.Lock()
handle.Release()
m.wlMutex.Unlock()
m.updateState()
})
})
}
func (m *Manager) updateState() {
m.headsMutex.RLock()
m.modesMutex.RLock()
outputs := make([]Output, 0)
for _, head := range m.heads {
if head.finished {
continue
}
modes := make([]OutputMode, 0)
var currentMode *OutputMode
for _, modeID := range head.modeIDs {
mode, exists := m.modes[modeID]
if !exists || 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,
ID: head.id,
}
outputs = append(outputs, output)
}
m.modesMutex.RUnlock()
m.headsMutex.RUnlock()
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
}
m.subMutex.RLock()
subCount := len(m.subscribers)
m.subMutex.RUnlock()
if subCount == 0 {
pending = false
continue
}
currentState := m.GetState()
if m.lastNotified != nil && !stateChanged(m.lastNotified, &currentState) {
pending = false
continue
}
m.subMutex.RLock()
for _, ch := range m.subscribers {
select {
case ch <- currentState:
default:
log.Warn("WlrOutput: subscriber channel full, dropping update")
}
}
m.subMutex.RUnlock()
stateCopy := currentState
m.lastNotified = &stateCopy
pending = false
}
}
}
func (m *Manager) Close() {
close(m.stopChan)
m.wg.Wait()
m.notifierWg.Wait()
m.subMutex.Lock()
for _, ch := range m.subscribers {
close(ch)
}
m.subscribers = make(map[string]chan State)
m.subMutex.Unlock()
m.modesMutex.Lock()
for _, mode := range m.modes {
if mode.handle != nil {
mode.handle.Release()
}
}
m.modes = make(map[uint32]*modeState)
m.modesMutex.Unlock()
m.headsMutex.Lock()
for _, head := range m.heads {
if head.handle != nil {
head.handle.Release()
}
}
m.heads = make(map[uint32]*headState)
m.headsMutex.Unlock()
if m.manager != nil {
m.manager.Stop()
}
}

View File

@@ -0,0 +1,191 @@
package wlroutput
import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlclient "github.com/yaslama/go-wayland/wayland/client"
)
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"`
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.Display
registry *wlclient.Registry
manager *wlr_output_management.ZwlrOutputManagerV1
headsMutex sync.RWMutex
heads map[uint32]*headState
modesMutex sync.RWMutex
modes map[uint32]*modeState
serial uint32
wlMutex sync.Mutex
cmdq chan cmd
stopChan chan struct{}
wg sync.WaitGroup
subscribers map[string]chan State
subMutex sync.RWMutex
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
finished 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.subMutex.Lock()
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch
}
func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok {
close(ch)
delete(m.subscribers, id)
}
m.subMutex.Unlock()
}
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 {
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
}