1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-16 01:02:46 -04:00

workspace/ext-ws: drop custom ext-workspace in favor of quickshell

WindowManager implementation
This commit is contained in:
bbedward
2026-05-14 10:34:35 -04:00
parent 71438530a8
commit fb5198fd0b
11 changed files with 48 additions and 3227 deletions
@@ -1,134 +0,0 @@
package extworkspace
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil {
models.RespondError(conn, req.ID, "extworkspace manager not initialized")
return
}
switch req.Method {
case "extworkspace.getState":
handleGetState(conn, req, manager)
case "extworkspace.activateWorkspace":
handleActivateWorkspace(conn, req, manager)
case "extworkspace.deactivateWorkspace":
handleDeactivateWorkspace(conn, req, manager)
case "extworkspace.removeWorkspace":
handleRemoveWorkspace(conn, req, manager)
case "extworkspace.createWorkspace":
handleCreateWorkspace(conn, req, manager)
case "extworkspace.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) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
}
func handleActivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID := models.GetOr(req, "groupID", "")
workspaceID, ok := models.Get[string](req, "workspaceID")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
return
}
if err := manager.ActivateWorkspace(groupID, workspaceID); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace activated"})
}
func handleDeactivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID := models.GetOr(req, "groupID", "")
workspaceID, ok := models.Get[string](req, "workspaceID")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
return
}
if err := manager.DeactivateWorkspace(groupID, workspaceID); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace deactivated"})
}
func handleRemoveWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID := models.GetOr(req, "groupID", "")
workspaceID, ok := models.Get[string](req, "workspaceID")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
return
}
if err := manager.RemoveWorkspace(groupID, workspaceID); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace removed"})
}
func handleCreateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
groupID, ok := models.Get[string](req, "groupID")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'groupID' parameter")
return
}
workspaceName, ok := models.Get[string](req, "name")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
return
}
if err := manager.CreateWorkspace(groupID, workspaceName); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace create requested"})
}
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
}
}
}
@@ -1,598 +0,0 @@
package extworkspace
import (
"fmt"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
func CheckCapability() bool {
display, err := wlclient.Connect("")
if err != nil {
return false
}
defer display.Destroy()
registry, err := display.GetRegistry()
if err != nil {
return false
}
defer registry.Destroy()
found := false
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
if e.Interface == ext_workspace.ExtWorkspaceManagerV1InterfaceName {
found = true
}
})
// Roundtrip to ensure all registry events are processed
if err := display.Roundtrip(); err != nil {
return false
}
return found
}
func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
m := &Manager{
display: display,
ctx: display.Context(),
cmdq: make(chan cmd, 128),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 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("ExtWorkspace 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) setupRegistry() error {
log.Info("ExtWorkspace: 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 == "wl_output" {
output := wlclient.NewOutput(m.ctx)
if err := registry.Bind(e.Name, e.Interface, 4, output); err == nil {
outputID := output.ID()
output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
m.outputNames.Store(outputID, ev.Name)
log.Debugf("ExtWorkspace: Output %d (%s) name received", outputID, ev.Name)
m.post(func() {
m.updateState()
})
})
}
return
}
if e.Interface == ext_workspace.ExtWorkspaceManagerV1InterfaceName {
log.Infof("ExtWorkspace: found %s", ext_workspace.ExtWorkspaceManagerV1InterfaceName)
manager := ext_workspace.NewExtWorkspaceManagerV1(m.ctx)
version := e.Version
if version > 1 {
version = 1
}
manager.SetWorkspaceGroupHandler(func(e ext_workspace.ExtWorkspaceManagerV1WorkspaceGroupEvent) {
m.handleWorkspaceGroup(e)
})
manager.SetWorkspaceHandler(func(e ext_workspace.ExtWorkspaceManagerV1WorkspaceEvent) {
m.handleWorkspace(e)
})
manager.SetDoneHandler(func(e ext_workspace.ExtWorkspaceManagerV1DoneEvent) {
log.Debug("ExtWorkspace: done event received")
m.post(func() {
m.updateState()
})
})
manager.SetFinishedHandler(func(e ext_workspace.ExtWorkspaceManagerV1FinishedEvent) {
log.Info("ExtWorkspace: finished event received")
})
if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil {
m.manager = manager
log.Info("ExtWorkspace: manager bound successfully")
} else {
log.Errorf("ExtWorkspace: failed to bind manager: %v", err)
}
}
})
log.Info("ExtWorkspace: registry setup complete (events will be processed async)")
return nil
}
func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1WorkspaceGroupEvent) {
handle := e.WorkspaceGroup
groupID := handle.ID()
log.Debugf("ExtWorkspace: New workspace group (id=%d)", groupID)
group := &workspaceGroupState{
id: groupID,
handle: handle,
outputIDs: make(map[uint32]bool),
workspaceIDs: make([]uint32, 0),
}
m.groups.Store(groupID, group)
handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1CapabilitiesEvent) {
log.Debugf("ExtWorkspace: Group %d capabilities: %d", groupID, e.Capabilities)
})
handle.SetOutputEnterHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1OutputEnterEvent) {
outputID := e.Output.ID()
log.Debugf("ExtWorkspace: Group %d output enter (output=%d)", groupID, outputID)
m.post(func() {
group.outputIDs[outputID] = true
m.updateState()
})
})
handle.SetOutputLeaveHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1OutputLeaveEvent) {
outputID := e.Output.ID()
log.Debugf("ExtWorkspace: Group %d output leave (output=%d)", groupID, outputID)
m.post(func() {
delete(group.outputIDs, outputID)
m.updateState()
})
})
handle.SetWorkspaceEnterHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1WorkspaceEnterEvent) {
workspaceID := e.Workspace.ID()
log.Debugf("ExtWorkspace: Group %d workspace enter (workspace=%d)", groupID, workspaceID)
m.post(func() {
if ws, ok := m.workspaces.Load(workspaceID); ok {
ws.groupID = groupID
}
group.workspaceIDs = append(group.workspaceIDs, workspaceID)
m.updateState()
})
})
handle.SetWorkspaceLeaveHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1WorkspaceLeaveEvent) {
workspaceID := e.Workspace.ID()
log.Debugf("ExtWorkspace: Group %d workspace leave (workspace=%d)", groupID, workspaceID)
m.post(func() {
if ws, ok := m.workspaces.Load(workspaceID); ok {
ws.groupID = 0
}
for i, id := range group.workspaceIDs {
if id == workspaceID {
group.workspaceIDs = append(group.workspaceIDs[:i], group.workspaceIDs[i+1:]...)
break
}
}
m.updateState()
})
})
handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1RemovedEvent) {
log.Debugf("ExtWorkspace: Group %d removed", groupID)
m.post(func() {
group.removed = true
m.groups.Delete(groupID)
m.wlMutex.Lock()
handle.Destroy()
m.wlMutex.Unlock()
m.updateState()
})
})
}
func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1WorkspaceEvent) {
handle := e.Workspace
workspaceID := handle.ID()
log.Debugf("ExtWorkspace: New workspace (proxy_id=%d)", workspaceID)
ws := &workspaceState{
id: workspaceID,
handle: handle,
coordinates: make([]uint32, 0),
}
m.workspaces.Store(workspaceID, ws)
handle.SetIdHandler(func(e ext_workspace.ExtWorkspaceHandleV1IdEvent) {
log.Debugf("ExtWorkspace: Workspace %d id: %s", workspaceID, e.Id)
m.post(func() {
ws.workspaceID = e.Id
m.updateState()
})
})
handle.SetNameHandler(func(e ext_workspace.ExtWorkspaceHandleV1NameEvent) {
log.Debugf("ExtWorkspace: Workspace %d name: %s", workspaceID, e.Name)
m.post(func() {
ws.name = e.Name
m.updateState()
})
})
handle.SetCoordinatesHandler(func(e ext_workspace.ExtWorkspaceHandleV1CoordinatesEvent) {
coords := make([]uint32, 0)
for i := 0; i < len(e.Coordinates); i += 4 {
if i+4 <= len(e.Coordinates) {
val := uint32(e.Coordinates[i]) |
uint32(e.Coordinates[i+1])<<8 |
uint32(e.Coordinates[i+2])<<16 |
uint32(e.Coordinates[i+3])<<24
coords = append(coords, val)
}
}
log.Debugf("ExtWorkspace: Workspace %d coordinates: %v", workspaceID, coords)
m.post(func() {
ws.coordinates = coords
m.updateState()
})
})
handle.SetStateHandler(func(e ext_workspace.ExtWorkspaceHandleV1StateEvent) {
log.Debugf("ExtWorkspace: Workspace %d state: %d", workspaceID, e.State)
m.post(func() {
ws.state = e.State
m.updateState()
})
})
handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceHandleV1CapabilitiesEvent) {
log.Debugf("ExtWorkspace: Workspace %d capabilities: %d", workspaceID, e.Capabilities)
})
handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceHandleV1RemovedEvent) {
log.Debugf("ExtWorkspace: Workspace %d removed", workspaceID)
m.post(func() {
ws.removed = true
m.workspaces.Delete(workspaceID)
m.wlMutex.Lock()
handle.Destroy()
m.wlMutex.Unlock()
m.updateState()
})
})
}
func (m *Manager) updateState() {
groups := make([]*WorkspaceGroup, 0)
m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
if group.removed {
return true
}
outputs := make([]string, 0)
for outputID := range group.outputIDs {
if name, ok := m.outputNames.Load(outputID); ok && name != "" {
outputs = append(outputs, name)
}
}
workspaces := make([]*Workspace, 0)
for _, wsID := range group.workspaceIDs {
ws, exists := m.workspaces.Load(wsID)
if !exists {
continue
}
if ws.removed {
continue
}
workspace := &Workspace{
ID: ws.workspaceID,
Name: ws.name,
Coordinates: ws.coordinates,
State: ws.state,
Active: ws.state&uint32(ext_workspace.ExtWorkspaceHandleV1StateActive) != 0,
Urgent: ws.state&uint32(ext_workspace.ExtWorkspaceHandleV1StateUrgent) != 0,
Hidden: ws.state&uint32(ext_workspace.ExtWorkspaceHandleV1StateHidden) != 0,
}
workspaces = append(workspaces, workspace)
}
groupState := &WorkspaceGroup{
ID: fmt.Sprintf("group-%d", group.id),
Outputs: outputs,
Workspaces: workspaces,
}
groups = append(groups, groupState)
return true
})
newState := State{
Groups: groups,
}
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, &currentState) {
pending = false
continue
}
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- currentState:
default:
log.Warn("ExtWorkspace: subscriber channel full, dropping update")
}
return true
})
stateCopy := currentState
m.lastNotified = &stateCopy
pending = false
}
}
}
func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error {
errChan := make(chan error, 1)
m.post(func() {
var targetGroupID uint32
if groupID != "" {
var parsedID uint32
if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil {
targetGroupID = parsedID
}
}
var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID {
return true
}
if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock()
err := ws.handle.Activate()
if err == nil {
err = m.manager.Commit()
}
m.wlMutex.Unlock()
errChan <- err
found = true
return false
}
return true
})
if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
})
return <-errChan
}
func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error {
errChan := make(chan error, 1)
m.post(func() {
var targetGroupID uint32
if groupID != "" {
var parsedID uint32
if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil {
targetGroupID = parsedID
}
}
var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID {
return true
}
if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock()
err := ws.handle.Deactivate()
if err == nil {
err = m.manager.Commit()
}
m.wlMutex.Unlock()
errChan <- err
found = true
return false
}
return true
})
if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
})
return <-errChan
}
func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error {
errChan := make(chan error, 1)
m.post(func() {
var targetGroupID uint32
if groupID != "" {
var parsedID uint32
if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil {
targetGroupID = parsedID
}
}
var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID {
return true
}
if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock()
err := ws.handle.Remove()
if err == nil {
err = m.manager.Commit()
}
m.wlMutex.Unlock()
errChan <- err
found = true
return false
}
return true
})
if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
})
return <-errChan
}
func (m *Manager) CreateWorkspace(groupID, workspaceName string) error {
errChan := make(chan error, 1)
m.post(func() {
var found bool
m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
if fmt.Sprintf("group-%d", group.id) == groupID {
m.wlMutex.Lock()
err := group.handle.CreateWorkspace(workspaceName)
if err == nil {
err = m.manager.Commit()
}
m.wlMutex.Unlock()
errChan <- err
found = true
return false
}
return true
})
if !found {
errChan <- fmt.Errorf("workspace group not found: %s", groupID)
}
})
return <-errChan
}
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.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if ws.handle != nil {
ws.handle.Destroy()
}
m.workspaces.Delete(key)
return true
})
m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
if group.handle != nil {
group.handle.Destroy()
}
m.groups.Delete(key)
return true
})
if m.manager != nil {
m.manager.Stop()
}
}
@@ -1,392 +0,0 @@
package extworkspace
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{Groups: []*WorkspaceGroup{}}
assert.True(t, stateChanged(s, nil))
assert.True(t, stateChanged(nil, s))
}
func TestStateChanged_GroupCountDiffers(t *testing.T) {
a := &State{Groups: []*WorkspaceGroup{{ID: "group-1"}}}
b := &State{Groups: []*WorkspaceGroup{}}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_GroupIDDiffers(t *testing.T) {
a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{}, Workspaces: []*Workspace{}}}}
b := &State{Groups: []*WorkspaceGroup{{ID: "group-2", Outputs: []string{}, Workspaces: []*Workspace{}}}}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_OutputCountDiffers(t *testing.T) {
a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}, Workspaces: []*Workspace{}}}}
b := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{}, Workspaces: []*Workspace{}}}}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_OutputNameDiffers(t *testing.T) {
a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}, Workspaces: []*Workspace{}}}}
b := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"HDMI-A-1"}, Workspaces: []*Workspace{}}}}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_WorkspaceCountDiffers(t *testing.T) {
a := &State{Groups: []*WorkspaceGroup{{
ID: "group-1",
Outputs: []string{},
Workspaces: []*Workspace{{ID: "1", Name: "ws1"}},
}}}
b := &State{Groups: []*WorkspaceGroup{{
ID: "group-1",
Outputs: []string{},
Workspaces: []*Workspace{},
}}}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_WorkspaceFieldsDiffer(t *testing.T) {
a := &State{Groups: []*WorkspaceGroup{{
ID: "group-1",
Outputs: []string{},
Workspaces: []*Workspace{{
ID: "1", Name: "ws1", State: 0, Active: false, Urgent: false, Hidden: false,
}},
}}}
b := &State{Groups: []*WorkspaceGroup{{
ID: "group-1",
Outputs: []string{},
Workspaces: []*Workspace{{
ID: "2", Name: "ws1", State: 0, Active: false, Urgent: false, Hidden: false,
}},
}}}
assert.True(t, stateChanged(a, b))
b.Groups[0].Workspaces[0].ID = "1"
b.Groups[0].Workspaces[0].Name = "ws2"
assert.True(t, stateChanged(a, b))
b.Groups[0].Workspaces[0].Name = "ws1"
b.Groups[0].Workspaces[0].State = 1
assert.True(t, stateChanged(a, b))
b.Groups[0].Workspaces[0].State = 0
b.Groups[0].Workspaces[0].Active = true
assert.True(t, stateChanged(a, b))
b.Groups[0].Workspaces[0].Active = false
b.Groups[0].Workspaces[0].Urgent = true
assert.True(t, stateChanged(a, b))
b.Groups[0].Workspaces[0].Urgent = false
b.Groups[0].Workspaces[0].Hidden = true
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_WorkspaceCoordinatesDiffer(t *testing.T) {
a := &State{Groups: []*WorkspaceGroup{{
ID: "group-1",
Outputs: []string{},
Workspaces: []*Workspace{{
ID: "1", Name: "ws1", Coordinates: []uint32{0, 0},
}},
}}}
b := &State{Groups: []*WorkspaceGroup{{
ID: "group-1",
Outputs: []string{},
Workspaces: []*Workspace{{
ID: "1", Name: "ws1", Coordinates: []uint32{1, 0},
}},
}}}
assert.True(t, stateChanged(a, b))
b.Groups[0].Workspaces[0].Coordinates = []uint32{0}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_Equal(t *testing.T) {
a := &State{Groups: []*WorkspaceGroup{{
ID: "group-1",
Outputs: []string{"eDP-1", "HDMI-A-1"},
Workspaces: []*Workspace{
{ID: "1", Name: "ws1", Coordinates: []uint32{0, 0}, State: 1, Active: true},
{ID: "2", Name: "ws2", Coordinates: []uint32{1, 0}, State: 0, Active: false},
},
}}}
b := &State{Groups: []*WorkspaceGroup{{
ID: "group-1",
Outputs: []string{"eDP-1", "HDMI-A-1"},
Workspaces: []*Workspace{
{ID: "1", Name: "ws1", Coordinates: []uint32{0, 0}, State: 1, Active: true},
{ID: "2", Name: "ws2", Coordinates: []uint32{1, 0}, State: 0, Active: false},
},
}}}
assert.False(t, stateChanged(a, b))
}
func TestManager_ConcurrentGetState(t *testing.T) {
m := &Manager{
state: &State{
Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}}},
},
}
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.Groups
}
}()
}
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{
Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}}},
}
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_SyncmapGroupsConcurrentAccess(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 := &workspaceGroupState{
id: key,
outputIDs: map[uint32]bool{1: true},
workspaceIDs: []uint32{uint32(j)},
}
m.groups.Store(key, state)
if loaded, ok := m.groups.Load(key); ok {
assert.Equal(t, key, loaded.id)
}
m.groups.Range(func(k uint32, v *workspaceGroupState) bool {
_ = v.id
_ = v.outputIDs
return true
})
}
m.groups.Delete(key)
}(i)
}
wg.Wait()
}
func TestManager_SyncmapWorkspacesConcurrentAccess(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 := &workspaceState{
id: key,
workspaceID: "ws-1",
name: "workspace",
state: uint32(j % 4),
coordinates: []uint32{uint32(j), 0},
}
m.workspaces.Store(key, state)
if loaded, ok := m.workspaces.Load(key); ok {
assert.Equal(t, key, loaded.id)
}
m.workspaces.Range(func(k uint32, v *workspaceState) bool {
_ = v.name
_ = v.state
return true
})
}
m.workspaces.Delete(key)
}(i)
}
wg.Wait()
}
func TestManager_SyncmapOutputNamesConcurrentAccess(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++ {
m.outputNames.Store(key, "eDP-1")
if loaded, ok := m.outputNames.Load(key); ok {
assert.NotEmpty(t, loaded)
}
m.outputNames.Range(func(k uint32, v string) bool {
_ = v
return true
})
}
m.outputNames.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.Groups)
assert.Empty(t, s.Groups)
}
func TestWorkspace_Fields(t *testing.T) {
ws := Workspace{
ID: "ws-1",
Name: "workspace 1",
Coordinates: []uint32{0, 0},
State: 1,
Active: true,
Urgent: false,
Hidden: false,
}
assert.Equal(t, "ws-1", ws.ID)
assert.Equal(t, "workspace 1", ws.Name)
assert.True(t, ws.Active)
assert.False(t, ws.Urgent)
assert.False(t, ws.Hidden)
}
func TestWorkspaceGroup_Fields(t *testing.T) {
group := WorkspaceGroup{
ID: "group-1",
Outputs: []string{"eDP-1", "HDMI-A-1"},
Workspaces: []*Workspace{
{ID: "ws-1", Name: "workspace 1"},
},
}
assert.Equal(t, "group-1", group.ID)
assert.Len(t, group.Outputs, 2)
assert.Len(t, group.Workspaces, 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"))
_, err := NewManager(mockDisplay)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to get registry")
}
-169
View File
@@ -1,169 +0,0 @@
package extworkspace
import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type Workspace struct {
ID string `json:"id"`
Name string `json:"name"`
Coordinates []uint32 `json:"coordinates"`
State uint32 `json:"state"`
Active bool `json:"active"`
Urgent bool `json:"urgent"`
Hidden bool `json:"hidden"`
}
type WorkspaceGroup struct {
ID string `json:"id"`
Outputs []string `json:"outputs"`
Workspaces []*Workspace `json:"workspaces"`
}
type State struct {
Groups []*WorkspaceGroup `json:"groups"`
}
type cmd struct {
fn func()
}
type Manager struct {
display wlclient.WaylandDisplay
ctx *wlclient.Context
registry *wlclient.Registry
manager *ext_workspace.ExtWorkspaceManagerV1
outputNames syncmap.Map[uint32, string]
groups syncmap.Map[uint32, *workspaceGroupState]
workspaces syncmap.Map[uint32, *workspaceState]
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
}
type workspaceGroupState struct {
id uint32
handle *ext_workspace.ExtWorkspaceGroupHandleV1
outputIDs map[uint32]bool
workspaceIDs []uint32
removed bool
}
type workspaceState struct {
id uint32
handle *ext_workspace.ExtWorkspaceHandleV1
workspaceID string
name string
coordinates []uint32
state uint32
groupID uint32
removed bool
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
if m.state == nil {
return State{
Groups: []*WorkspaceGroup{},
}
}
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 ch, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch)
}
}
func (m *Manager) notifySubscribers() {
select {
case m.dirty <- struct{}{}:
default:
}
}
func stateChanged(old, new *State) bool {
if old == nil || new == nil {
return true
}
if len(old.Groups) != len(new.Groups) {
return true
}
for i, newGroup := range new.Groups {
if i >= len(old.Groups) {
return true
}
oldGroup := old.Groups[i]
if oldGroup.ID != newGroup.ID {
return true
}
if len(oldGroup.Outputs) != len(newGroup.Outputs) {
return true
}
for j, newOutput := range newGroup.Outputs {
if j >= len(oldGroup.Outputs) {
return true
}
if oldGroup.Outputs[j] != newOutput {
return true
}
}
if len(oldGroup.Workspaces) != len(newGroup.Workspaces) {
return true
}
for j, newWs := range newGroup.Workspaces {
if j >= len(oldGroup.Workspaces) {
return true
}
oldWs := oldGroup.Workspaces[j]
if oldWs.ID != newWs.ID || oldWs.Name != newWs.Name || oldWs.State != newWs.State {
return true
}
if oldWs.Active != newWs.Active || oldWs.Urgent != newWs.Urgent || oldWs.Hidden != newWs.Hidden {
return true
}
if len(oldWs.Coordinates) != len(newWs.Coordinates) {
return true
}
for k, coord := range newWs.Coordinates {
if k >= len(oldWs.Coordinates) {
return true
}
if oldWs.Coordinates[k] != coord {
return true
}
}
}
}
return false
}
-22
View File
@@ -13,7 +13,6 @@ import (
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
@@ -138,27 +137,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
return
}
if strings.HasPrefix(req.Method, "extworkspace.") {
if extWorkspaceManager == nil {
if extWorkspaceAvailable.Load() {
extWorkspaceInitMutex.Lock()
if extWorkspaceManager == nil {
if err := InitializeExtWorkspaceManager(); err != nil {
extWorkspaceInitMutex.Unlock()
models.RespondError(conn, req.ID, "extworkspace manager not available")
return
}
}
extWorkspaceInitMutex.Unlock()
} else {
models.RespondError(conn, req.ID, "extworkspace manager not initialized")
return
}
}
extworkspace.HandleRequest(conn, req, extWorkspaceManager)
return
}
if strings.HasPrefix(req.Method, "wlroutput.") {
if wlrOutputManager == nil {
models.RespondError(conn, req.ID, "wlroutput manager not initialized")
-98
View File
@@ -24,7 +24,6 @@ import (
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
@@ -68,7 +67,6 @@ var appPickerManager *apppicker.Manager
var cupsManager *cups.Manager
var tailscaleManager *tailscale.Manager
var dwlManager *dwl.Manager
var extWorkspaceManager *extworkspace.Manager
var brightnessManager *brightness.Manager
var wlrOutputManager *wlroutput.Manager
var evdevManager *evdev.Manager
@@ -86,8 +84,6 @@ const dbusClientID = "dms-dbus-client"
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
var cupsSubscribers syncmap.Map[string, bool]
var cupsSubscriberCount atomic.Int32
var extWorkspaceAvailable atomic.Bool
var extWorkspaceInitMutex sync.Mutex
func getSocketDir() string {
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
@@ -293,30 +289,6 @@ func InitializeBrightnessManager() error {
return nil
}
func InitializeExtWorkspaceManager() error {
log.Info("Attempting to initialize ExtWorkspace...")
if wlContext == nil {
ctx, err := wlcontext.New()
if err != nil {
log.Errorf("Failed to create shared Wayland context: %v", err)
return err
}
wlContext = ctx
}
manager, err := extworkspace.NewManager(wlContext.Display())
if err != nil {
log.Debug("Failed to initialize extworkspace manager: %v", err)
return err
}
extWorkspaceManager = manager
log.Info("ExtWorkspace initialized successfully")
return nil
}
func InitializeWlrOutputManager() error {
log.Info("Attempting to initialize WlrOutput management...")
@@ -499,10 +471,6 @@ func getCapabilities() Capabilities {
caps = append(caps, "dwl")
}
if extWorkspaceAvailable.Load() {
caps = append(caps, "extworkspace")
}
if brightnessManager != nil {
caps = append(caps, "brightness")
}
@@ -573,10 +541,6 @@ func getServerInfo() ServerInfo {
caps = append(caps, "dwl")
}
if extWorkspaceAvailable.Load() {
caps = append(caps, "extworkspace")
}
if brightnessManager != nil {
caps = append(caps, "brightness")
}
@@ -1113,50 +1077,6 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}()
}
if shouldSubscribe("extworkspace") {
if extWorkspaceManager == nil && extWorkspaceAvailable.Load() {
extWorkspaceInitMutex.Lock()
if extWorkspaceManager == nil {
if err := InitializeExtWorkspaceManager(); err != nil {
log.Warnf("Failed to initialize ExtWorkspace manager for subscription: %v", err)
}
}
extWorkspaceInitMutex.Unlock()
}
if extWorkspaceManager != nil {
wg.Add(1)
extWorkspaceChan := extWorkspaceManager.Subscribe(clientID + "-extworkspace")
go func() {
defer wg.Done()
defer extWorkspaceManager.Unsubscribe(clientID + "-extworkspace")
initialState := extWorkspaceManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "extworkspace", Data: initialState}:
case <-stopChan:
return
}
for {
select {
case state, ok := <-extWorkspaceChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "extworkspace", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
}
if shouldSubscribe("brightness") && brightnessManager != nil {
wg.Add(2)
brightnessStateChan := brightnessManager.Subscribe(clientID + "-brightness-state")
@@ -1415,9 +1335,6 @@ func cleanupManagers() {
if dwlManager != nil {
dwlManager.Close()
}
if extWorkspaceManager != nil {
extWorkspaceManager.Close()
}
if brightnessManager != nil {
brightnessManager.Close()
}
@@ -1597,13 +1514,6 @@ func Start(printDocs bool) error {
log.Info(" - appId : Focused window app ID")
log.Info(" - kbLayout : Current keyboard layout")
log.Info(" - keymode : Current keybind mode")
log.Info("ExtWorkspace:")
log.Info(" extworkspace.getState - Get current workspace state (groups, workspaces)")
log.Info(" extworkspace.activateWorkspace - Activate workspace (params: groupID, workspaceID)")
log.Info(" extworkspace.deactivateWorkspace - Deactivate workspace (params: groupID, workspaceID)")
log.Info(" extworkspace.removeWorkspace - Remove workspace (params: groupID, workspaceID)")
log.Info(" extworkspace.createWorkspace - Create workspace (params: groupID, name)")
log.Info(" extworkspace.subscribe - Subscribe to workspace state changes (streaming)")
log.Info("Brightness:")
log.Info(" brightness.getState - Get current brightness state for all devices")
log.Info(" brightness.setBrightness - Set device brightness (params: device, percent)")
@@ -1784,14 +1694,6 @@ func Start(printDocs bool) error {
log.Debugf("DWL manager unavailable: %v", err)
}
if extworkspace.CheckCapability() {
extWorkspaceAvailable.Store(true)
log.Info("ExtWorkspace capability detected and will be available on subscription")
} else {
log.Debug("ExtWorkspace capability not available")
extWorkspaceAvailable.Store(false)
}
if err := InitializeWlrOutputManager(); err != nil {
log.Debugf("WlrOutput manager unavailable: %v", err)
}