mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
279 lines
7.3 KiB
Go
279 lines
7.3 KiB
Go
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]any `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)")
|
|
})
|
|
|
|
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 {
|
|
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
|
|
}
|