summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl
diff options
context:
space:
mode:
authorAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
committerAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
commit70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch)
treeab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl
parent5d94c0a7d44a2255b81815a52a7056a94a39842d (diff)
downloadRaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz
RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/handlers.go138
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager.go522
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager_test.go366
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/types.go176
4 files changed, 1202 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/handlers.go
new file mode 100644
index 0000000..e83cc84
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/handlers.go
@@ -0,0 +1,138 @@
+package dwl
+
+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, "dwl manager not initialized")
+ return
+ }
+
+ switch req.Method {
+ case "dwl.getState":
+ handleGetState(conn, req, manager)
+ case "dwl.setTags":
+ handleSetTags(conn, req, manager)
+ case "dwl.setClientTags":
+ handleSetClientTags(conn, req, manager)
+ case "dwl.setLayout":
+ handleSetLayout(conn, req, manager)
+ case "dwl.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 handleSetTags(conn net.Conn, req models.Request, manager *Manager) {
+ output, ok := models.Get[string](req, "output")
+ if !ok {
+ models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
+ return
+ }
+
+ tagmask, ok := models.Get[float64](req, "tagmask")
+ if !ok {
+ models.RespondError(conn, req.ID, "missing or invalid 'tagmask' parameter")
+ return
+ }
+
+ toggleTagset, ok := models.Get[float64](req, "toggleTagset")
+ if !ok {
+ models.RespondError(conn, req.ID, "missing or invalid 'toggleTagset' parameter")
+ return
+ }
+
+ if err := manager.SetTags(output, uint32(tagmask), uint32(toggleTagset)); err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "tags set"})
+}
+
+func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) {
+ output, ok := models.Get[string](req, "output")
+ if !ok {
+ models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
+ return
+ }
+
+ andTags, ok := models.Get[float64](req, "andTags")
+ if !ok {
+ models.RespondError(conn, req.ID, "missing or invalid 'andTags' parameter")
+ return
+ }
+
+ xorTags, ok := models.Get[float64](req, "xorTags")
+ if !ok {
+ models.RespondError(conn, req.ID, "missing or invalid 'xorTags' parameter")
+ return
+ }
+
+ if err := manager.SetClientTags(output, uint32(andTags), uint32(xorTags)); err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "client tags set"})
+}
+
+func handleSetLayout(conn net.Conn, req models.Request, manager *Manager) {
+ output, ok := models.Get[string](req, "output")
+ if !ok {
+ models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
+ return
+ }
+
+ index, ok := models.Get[float64](req, "index")
+ if !ok {
+ models.RespondError(conn, req.ID, "missing or invalid 'index' parameter")
+ return
+ }
+
+ if err := manager.SetLayout(output, uint32(index)); err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "layout set"})
+}
+
+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
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager.go
new file mode 100644
index 0000000..0d21afa
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager.go
@@ -0,0 +1,522 @@
+package dwl
+
+import (
+ "fmt"
+ "time"
+
+ wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
+)
+
+func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
+ m := &Manager{
+ display: display,
+ ctx: display.Context(),
+ cmdq: make(chan cmd, 128),
+ outputSetupReq: make(chan uint32, 16),
+ stopChan: make(chan struct{}),
+
+ dirty: make(chan struct{}, 1),
+ layouts: make([]string, 0),
+ }
+
+ if err := m.setupRegistry(); err != nil {
+ return nil, err
+ }
+
+ m.updateState()
+
+ m.notifierWg.Add(1)
+ go m.notifier()
+
+ m.wg.Add(1)
+ go m.waylandActor()
+
+ return m, nil
+}
+
+func (m *Manager) post(fn func()) {
+ select {
+ case m.cmdq <- cmd{fn: fn}:
+ default:
+ log.Warn("DWL 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()
+ case outputID := <-m.outputSetupReq:
+ out, exists := m.outputs.Load(outputID)
+ if !exists {
+ log.Warnf("DWL: Output %d no longer exists, skipping setup", outputID)
+ continue
+ }
+
+ if out.ipcOutput != nil {
+ continue
+ }
+
+ mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2)
+ if !ok || mgr == nil {
+ log.Errorf("DWL: Manager not available for output %d setup", outputID)
+ continue
+ }
+
+ log.Infof("DWL: Setting up ipcOutput for dynamically added output %d", outputID)
+ if err := m.setupOutput(mgr, out.output); err != nil {
+ log.Errorf("DWL: Failed to setup output %d: %v", outputID, err)
+ } else {
+ m.updateState()
+ }
+ }
+ }
+}
+
+func (m *Manager) setupRegistry() error {
+ log.Info("DWL: starting registry setup")
+
+ registry, err := m.display.GetRegistry()
+ if err != nil {
+ return fmt.Errorf("failed to get registry: %w", err)
+ }
+ m.registry = registry
+
+ outputs := make([]*wlclient.Output, 0)
+ outputRegNames := make(map[uint32]uint32)
+ var dwlMgr *dwl_ipc.ZdwlIpcManagerV2
+
+ registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
+ switch e.Interface {
+ case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
+ log.Infof("DWL: found %s", dwl_ipc.ZdwlIpcManagerV2InterfaceName)
+ manager := dwl_ipc.NewZdwlIpcManagerV2(m.ctx)
+ version := e.Version
+ if version > 2 {
+ version = 2
+ }
+ if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil {
+ dwlMgr = manager
+ log.Info("DWL: manager bound successfully")
+
+ // Set handlers immediately after binding, before roundtrips
+ manager.SetTagsHandler(func(e dwl_ipc.ZdwlIpcManagerV2TagsEvent) {
+ log.Infof("DWL: Tags count: %d", e.Amount)
+ m.tagCount = e.Amount
+ m.updateState()
+ })
+
+ manager.SetLayoutHandler(func(e dwl_ipc.ZdwlIpcManagerV2LayoutEvent) {
+ log.Infof("DWL: Layout: %s", e.Name)
+ m.layouts = append(m.layouts, e.Name)
+ m.updateState()
+ })
+ } else {
+ log.Errorf("DWL: failed to bind manager: %v", err)
+ }
+ case "wl_output":
+ log.Debugf("DWL: found wl_output (name=%d)", e.Name)
+ output := wlclient.NewOutput(m.ctx)
+
+ outState := &outputState{
+ registryName: e.Name,
+ output: output,
+ tags: make([]TagState, 0),
+ }
+
+ output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
+ log.Debugf("DWL: Output name: %s (registry=%d)", ev.Name, e.Name)
+ outState.name = ev.Name
+ })
+
+ output.SetDescriptionHandler(func(ev wlclient.OutputDescriptionEvent) {
+ log.Debugf("DWL: Output description: %s", ev.Description)
+ })
+
+ version := e.Version
+ if version > 4 {
+ version = 4
+ }
+ if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
+ outputID := output.ID()
+ outState.id = outputID
+ log.Infof("DWL: Bound wl_output id=%d registry_name=%d", outputID, e.Name)
+ outputs = append(outputs, output)
+ outputRegNames[outputID] = e.Name
+
+ m.outputs.Store(outputID, outState)
+
+ if m.manager != nil {
+ select {
+ case m.outputSetupReq <- outputID:
+ log.Debugf("DWL: Queued setup for output %d", outputID)
+ default:
+ log.Warnf("DWL: Setup queue full, output %d will not be initialized", outputID)
+ }
+ }
+ } else {
+ log.Errorf("DWL: Failed to bind wl_output: %v", err)
+ }
+ }
+ })
+
+ registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
+ m.post(func() {
+ var outToRelease *outputState
+ m.outputs.Range(func(id uint32, out *outputState) bool {
+ if out.registryName == e.Name {
+ log.Infof("DWL: Output %d removed", id)
+ outToRelease = out
+ m.outputs.Delete(id)
+ return false
+ }
+ return true
+ })
+
+ if outToRelease != nil {
+ if ipcOut, ok := outToRelease.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok && ipcOut != nil {
+ m.wlMutex.Lock()
+ ipcOut.Release()
+ m.wlMutex.Unlock()
+ log.Debugf("DWL: Released ipcOutput for removed output %d", outToRelease.id)
+ }
+ m.updateState()
+ }
+ })
+ })
+
+ if err := m.display.Roundtrip(); err != nil {
+ return fmt.Errorf("first roundtrip failed: %w", err)
+ }
+ if err := m.display.Roundtrip(); err != nil {
+ return fmt.Errorf("second roundtrip failed: %w", err)
+ }
+
+ if dwlMgr == nil {
+ log.Info("DWL: manager not found in registry")
+ return fmt.Errorf("dwl_ipc_manager_v2 not available")
+ }
+
+ m.manager = dwlMgr
+
+ for _, output := range outputs {
+ if err := m.setupOutput(dwlMgr, output); err != nil {
+ log.Warnf("DWL: Failed to setup output %d: %v", output.ID(), err)
+ }
+ }
+
+ if err := m.display.Roundtrip(); err != nil {
+ return fmt.Errorf("final roundtrip failed: %w", err)
+ }
+
+ log.Info("DWL: registry setup complete")
+ return nil
+}
+
+func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclient.Output) error {
+ m.wlMutex.Lock()
+ ipcOutput, err := manager.GetOutput(output)
+ m.wlMutex.Unlock()
+ if err != nil {
+ return fmt.Errorf("failed to get dwl output: %w", err)
+ }
+
+ outState, exists := m.outputs.Load(output.ID())
+ if !exists {
+ return fmt.Errorf("output state not found for id %d", output.ID())
+ }
+ outState.ipcOutput = ipcOutput
+
+ ipcOutput.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
+ outState.active = e.Active
+ })
+
+ ipcOutput.SetTagHandler(func(e dwl_ipc.ZdwlIpcOutputV2TagEvent) {
+ updated := false
+ for i, tag := range outState.tags {
+ if tag.Tag == e.Tag {
+ outState.tags[i] = TagState{
+ Tag: e.Tag,
+ State: e.State,
+ Clients: e.Clients,
+ Focused: e.Focused,
+ }
+ updated = true
+ break
+ }
+ }
+
+ if !updated {
+ outState.tags = append(outState.tags, TagState{
+ Tag: e.Tag,
+ State: e.State,
+ Clients: e.Clients,
+ Focused: e.Focused,
+ })
+ }
+
+ m.updateState()
+ })
+
+ ipcOutput.SetLayoutHandler(func(e dwl_ipc.ZdwlIpcOutputV2LayoutEvent) {
+ outState.layout = e.Layout
+ })
+
+ ipcOutput.SetTitleHandler(func(e dwl_ipc.ZdwlIpcOutputV2TitleEvent) {
+ outState.title = e.Title
+ })
+
+ ipcOutput.SetAppidHandler(func(e dwl_ipc.ZdwlIpcOutputV2AppidEvent) {
+ outState.appID = e.Appid
+ })
+
+ ipcOutput.SetLayoutSymbolHandler(func(e dwl_ipc.ZdwlIpcOutputV2LayoutSymbolEvent) {
+ outState.layoutSymbol = e.Layout
+ })
+
+ ipcOutput.SetKbLayoutHandler(func(e dwl_ipc.ZdwlIpcOutputV2KbLayoutEvent) {
+ outState.kbLayout = e.KbLayout
+ })
+
+ ipcOutput.SetKeymodeHandler(func(e dwl_ipc.ZdwlIpcOutputV2KeymodeEvent) {
+ outState.keymode = e.Keymode
+ })
+
+ ipcOutput.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
+ m.updateState()
+ })
+
+ return nil
+}
+
+func (m *Manager) updateState() {
+ outputs := make(map[string]*OutputState)
+ activeOutput := ""
+
+ m.outputs.Range(func(key uint32, out *outputState) bool {
+ name := out.name
+ if name == "" {
+ name = fmt.Sprintf("output-%d", out.id)
+ }
+
+ tagsCopy := make([]TagState, len(out.tags))
+ copy(tagsCopy, out.tags)
+
+ outputs[name] = &OutputState{
+ Name: name,
+ Active: out.active,
+ Tags: tagsCopy,
+ Layout: out.layout,
+ LayoutSymbol: out.layoutSymbol,
+ Title: out.title,
+ AppID: out.appID,
+ KbLayout: out.kbLayout,
+ Keymode: out.keymode,
+ }
+
+ if out.active != 0 {
+ activeOutput = name
+ }
+ return true
+ })
+
+ newState := State{
+ Outputs: outputs,
+ TagCount: m.tagCount,
+ Layouts: m.layouts,
+ ActiveOutput: activeOutput,
+ }
+
+ 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("DWL: subscriber channel full, dropping update")
+ }
+ return true
+ })
+
+ stateCopy := currentState
+ m.lastNotified = &stateCopy
+ pending = false
+ }
+ }
+}
+
+func (m *Manager) ensureOutputSetup(out *outputState) error {
+ if out.ipcOutput != nil {
+ return nil
+ }
+
+ return fmt.Errorf("output not yet initialized - setup in progress, retry in a moment")
+}
+
+func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32) error {
+ availableOutputs := make([]string, 0)
+ var targetOut *outputState
+ m.outputs.Range(func(key uint32, out *outputState) bool {
+ name := out.name
+ if name == "" {
+ name = fmt.Sprintf("output-%d", out.id)
+ }
+ availableOutputs = append(availableOutputs, name)
+ if name == outputName {
+ targetOut = out
+ return false
+ }
+ return true
+ })
+
+ if targetOut == nil {
+ return fmt.Errorf("output not found: %s (available: %v)", outputName, availableOutputs)
+ }
+
+ if err := m.ensureOutputSetup(targetOut); err != nil {
+ return fmt.Errorf("failed to setup output %s: %w", outputName, err)
+ }
+
+ ipcOut, ok := targetOut.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2)
+ if !ok {
+ return fmt.Errorf("output %s has invalid ipcOutput type", outputName)
+ }
+
+ m.wlMutex.Lock()
+ err := ipcOut.SetTags(tagmask, toggleTagset)
+ m.wlMutex.Unlock()
+ return err
+}
+
+func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint32) error {
+ var targetOut *outputState
+ m.outputs.Range(func(key uint32, out *outputState) bool {
+ name := out.name
+ if name == "" {
+ name = fmt.Sprintf("output-%d", out.id)
+ }
+ if name == outputName {
+ targetOut = out
+ return false
+ }
+ return true
+ })
+
+ if targetOut == nil {
+ return fmt.Errorf("output not found: %s", outputName)
+ }
+
+ if err := m.ensureOutputSetup(targetOut); err != nil {
+ return fmt.Errorf("failed to setup output %s: %w", outputName, err)
+ }
+
+ ipcOut, ok := targetOut.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2)
+ if !ok {
+ return fmt.Errorf("output %s has invalid ipcOutput type", outputName)
+ }
+
+ m.wlMutex.Lock()
+ err := ipcOut.SetClientTags(andTags, xorTags)
+ m.wlMutex.Unlock()
+ return err
+}
+
+func (m *Manager) SetLayout(outputName string, index uint32) error {
+ var targetOut *outputState
+ m.outputs.Range(func(key uint32, out *outputState) bool {
+ name := out.name
+ if name == "" {
+ name = fmt.Sprintf("output-%d", out.id)
+ }
+ if name == outputName {
+ targetOut = out
+ return false
+ }
+ return true
+ })
+
+ if targetOut == nil {
+ return fmt.Errorf("output not found: %s", outputName)
+ }
+
+ if err := m.ensureOutputSetup(targetOut); err != nil {
+ return fmt.Errorf("failed to setup output %s: %w", outputName, err)
+ }
+
+ ipcOut, ok := targetOut.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2)
+ if !ok {
+ return fmt.Errorf("output %s has invalid ipcOutput type", outputName)
+ }
+
+ m.wlMutex.Lock()
+ err := ipcOut.SetLayout(index)
+ m.wlMutex.Unlock()
+ return err
+}
+
+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.outputs.Range(func(key uint32, out *outputState) bool {
+ if ipcOut, ok := out.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok {
+ ipcOut.Release()
+ }
+ m.outputs.Delete(key)
+ return true
+ })
+
+ if mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2); ok {
+ mgr.Release()
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager_test.go
new file mode 100644
index 0000000..13af137
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/manager_test.go
@@ -0,0 +1,366 @@
+package dwl
+
+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{TagCount: 9}
+ assert.True(t, stateChanged(s, nil))
+ assert.True(t, stateChanged(nil, s))
+}
+
+func TestStateChanged_TagCountDiffers(t *testing.T) {
+ a := &State{TagCount: 9, Outputs: make(map[string]*OutputState), Layouts: []string{}}
+ b := &State{TagCount: 10, Outputs: make(map[string]*OutputState), Layouts: []string{}}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_LayoutLengthDiffers(t *testing.T) {
+ a := &State{TagCount: 9, Layouts: []string{"tile"}, Outputs: make(map[string]*OutputState)}
+ b := &State{TagCount: 9, Layouts: []string{"tile", "monocle"}, Outputs: make(map[string]*OutputState)}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_ActiveOutputDiffers(t *testing.T) {
+ a := &State{TagCount: 9, ActiveOutput: "eDP-1", Outputs: make(map[string]*OutputState), Layouts: []string{}}
+ b := &State{TagCount: 9, ActiveOutput: "HDMI-A-1", Outputs: make(map[string]*OutputState), Layouts: []string{}}
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_OutputCountDiffers(t *testing.T) {
+ a := &State{
+ TagCount: 9,
+ Outputs: map[string]*OutputState{"eDP-1": {}},
+ Layouts: []string{},
+ }
+ b := &State{
+ TagCount: 9,
+ Outputs: map[string]*OutputState{},
+ Layouts: []string{},
+ }
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_OutputFieldsDiffer(t *testing.T) {
+ a := &State{
+ TagCount: 9,
+ Layouts: []string{},
+ Outputs: map[string]*OutputState{
+ "eDP-1": {Active: 1, Layout: 0, Title: "Firefox"},
+ },
+ }
+ b := &State{
+ TagCount: 9,
+ Layouts: []string{},
+ Outputs: map[string]*OutputState{
+ "eDP-1": {Active: 0, Layout: 0, Title: "Firefox"},
+ },
+ }
+ assert.True(t, stateChanged(a, b))
+
+ b.Outputs["eDP-1"].Active = 1
+ b.Outputs["eDP-1"].Layout = 1
+ assert.True(t, stateChanged(a, b))
+
+ b.Outputs["eDP-1"].Layout = 0
+ b.Outputs["eDP-1"].Title = "Code"
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_TagsDiffer(t *testing.T) {
+ a := &State{
+ TagCount: 9,
+ Layouts: []string{},
+ Outputs: map[string]*OutputState{
+ "eDP-1": {Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}}},
+ },
+ }
+ b := &State{
+ TagCount: 9,
+ Layouts: []string{},
+ Outputs: map[string]*OutputState{
+ "eDP-1": {Tags: []TagState{{Tag: 1, State: 2, Clients: 2, Focused: 1}}},
+ },
+ }
+ assert.True(t, stateChanged(a, b))
+
+ b.Outputs["eDP-1"].Tags[0].State = 1
+ b.Outputs["eDP-1"].Tags[0].Clients = 3
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_Equal(t *testing.T) {
+ a := &State{
+ TagCount: 9,
+ ActiveOutput: "eDP-1",
+ Layouts: []string{"tile", "monocle"},
+ Outputs: map[string]*OutputState{
+ "eDP-1": {
+ Name: "eDP-1",
+ Active: 1,
+ Layout: 0,
+ LayoutSymbol: "[]=",
+ Title: "Firefox",
+ AppID: "firefox",
+ KbLayout: "us",
+ Keymode: "",
+ Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}},
+ },
+ },
+ }
+ b := &State{
+ TagCount: 9,
+ ActiveOutput: "eDP-1",
+ Layouts: []string{"tile", "monocle"},
+ Outputs: map[string]*OutputState{
+ "eDP-1": {
+ Name: "eDP-1",
+ Active: 1,
+ Layout: 0,
+ LayoutSymbol: "[]=",
+ Title: "Firefox",
+ AppID: "firefox",
+ KbLayout: "us",
+ Keymode: "",
+ Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}},
+ },
+ },
+ }
+ assert.False(t, stateChanged(a, b))
+}
+
+func TestManager_ConcurrentGetState(t *testing.T) {
+ m := &Manager{
+ state: &State{
+ TagCount: 9,
+ Layouts: []string{"tile"},
+ Outputs: map[string]*OutputState{"eDP-1": {Name: "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.TagCount
+ _ = 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{
+ TagCount: uint32(j % 10),
+ Layouts: []string{"tile", "monocle"},
+ Outputs: map[string]*OutputState{"eDP-1": {Active: uint32(j % 2)}},
+ }
+ 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_SyncmapOutputsConcurrentAccess(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 := &outputState{
+ id: key,
+ name: "test-output",
+ active: uint32(j % 2),
+ tags: []TagState{{Tag: uint32(j), State: 1}},
+ }
+ m.outputs.Store(key, state)
+
+ if loaded, ok := m.outputs.Load(key); ok {
+ assert.Equal(t, key, loaded.id)
+ }
+
+ m.outputs.Range(func(k uint32, v *outputState) bool {
+ _ = v.name
+ _ = v.active
+ return true
+ })
+ }
+
+ m.outputs.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.NotNil(t, s.Layouts)
+ assert.Equal(t, uint32(0), s.TagCount)
+}
+
+func TestTagState_Fields(t *testing.T) {
+ tag := TagState{
+ Tag: 1,
+ State: 2,
+ Clients: 3,
+ Focused: 1,
+ }
+
+ assert.Equal(t, uint32(1), tag.Tag)
+ assert.Equal(t, uint32(2), tag.State)
+ assert.Equal(t, uint32(3), tag.Clients)
+ assert.Equal(t, uint32(1), tag.Focused)
+}
+
+func TestOutputState_Fields(t *testing.T) {
+ out := OutputState{
+ Name: "eDP-1",
+ Active: 1,
+ Tags: []TagState{{Tag: 1}},
+ Layout: 0,
+ LayoutSymbol: "[]=",
+ Title: "Firefox",
+ AppID: "firefox",
+ KbLayout: "us",
+ Keymode: "",
+ }
+
+ assert.Equal(t, "eDP-1", out.Name)
+ assert.Equal(t, uint32(1), out.Active)
+ assert.Len(t, out.Tags, 1)
+ assert.Equal(t, "[]=", out.LayoutSymbol)
+}
+
+func TestStateChanged_NewOutputAppears(t *testing.T) {
+ a := &State{
+ TagCount: 9,
+ Layouts: []string{},
+ Outputs: map[string]*OutputState{
+ "eDP-1": {Name: "eDP-1"},
+ },
+ }
+ b := &State{
+ TagCount: 9,
+ Layouts: []string{},
+ Outputs: map[string]*OutputState{
+ "eDP-1": {Name: "eDP-1"},
+ "HDMI-A-1": {Name: "HDMI-A-1"},
+ },
+ }
+ assert.True(t, stateChanged(a, b))
+}
+
+func TestStateChanged_TagsLengthDiffers(t *testing.T) {
+ a := &State{
+ TagCount: 9,
+ Layouts: []string{},
+ Outputs: map[string]*OutputState{
+ "eDP-1": {Tags: []TagState{{Tag: 1}}},
+ },
+ }
+ b := &State{
+ TagCount: 9,
+ Layouts: []string{},
+ Outputs: map[string]*OutputState{
+ "eDP-1": {Tags: []TagState{{Tag: 1}, {Tag: 2}}},
+ },
+ }
+ 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/dwl/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/types.go
new file mode 100644
index 0000000..2b17537
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl/types.go
@@ -0,0 +1,176 @@
+package dwl
+
+import (
+ "sync"
+
+ wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
+ "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
+)
+
+type TagState struct {
+ Tag uint32 `json:"tag"`
+ State uint32 `json:"state"`
+ Clients uint32 `json:"clients"`
+ Focused uint32 `json:"focused"`
+}
+
+type OutputState struct {
+ Name string `json:"name"`
+ Active uint32 `json:"active"`
+ Tags []TagState `json:"tags"`
+ Layout uint32 `json:"layout"`
+ LayoutSymbol string `json:"layoutSymbol"`
+ Title string `json:"title"`
+ AppID string `json:"appId"`
+ KbLayout string `json:"kbLayout"`
+ Keymode string `json:"keymode"`
+}
+
+type State struct {
+ Outputs map[string]*OutputState `json:"outputs"`
+ TagCount uint32 `json:"tagCount"`
+ Layouts []string `json:"layouts"`
+ ActiveOutput string `json:"activeOutput"`
+}
+
+type cmd struct {
+ fn func()
+}
+
+type Manager struct {
+ display wlclient.WaylandDisplay
+ ctx *wlclient.Context
+ registry *wlclient.Registry
+ manager any
+
+ outputs syncmap.Map[uint32, *outputState]
+
+ tagCount uint32
+ layouts []string
+
+ wlMutex sync.Mutex
+ cmdq chan cmd
+ outputSetupReq chan uint32
+ 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 outputState struct {
+ id uint32
+ registryName uint32
+ output *wlclient.Output
+ ipcOutput any
+ name string
+ active uint32
+ tags []TagState
+ layout uint32
+ layoutSymbol string
+ title string
+ appID string
+ kbLayout string
+ keymode string
+}
+
+func (m *Manager) GetState() State {
+ m.stateMutex.RLock()
+ defer m.stateMutex.RUnlock()
+ if m.state == nil {
+ return State{
+ Outputs: make(map[string]*OutputState),
+ Layouts: []string{},
+ TagCount: 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 stateChanged(old, new *State) bool {
+ if old == nil || new == nil {
+ return true
+ }
+ if old.TagCount != new.TagCount {
+ return true
+ }
+ if len(old.Layouts) != len(new.Layouts) {
+ return true
+ }
+ if old.ActiveOutput != new.ActiveOutput {
+ return true
+ }
+ if len(old.Outputs) != len(new.Outputs) {
+ return true
+ }
+
+ for name, newOut := range new.Outputs {
+ oldOut, exists := old.Outputs[name]
+ if !exists {
+ return true
+ }
+ if oldOut.Active != newOut.Active {
+ return true
+ }
+ if oldOut.Layout != newOut.Layout {
+ return true
+ }
+ if oldOut.LayoutSymbol != newOut.LayoutSymbol {
+ return true
+ }
+ if oldOut.Title != newOut.Title {
+ return true
+ }
+ if oldOut.AppID != newOut.AppID {
+ return true
+ }
+ if oldOut.KbLayout != newOut.KbLayout {
+ return true
+ }
+ if oldOut.Keymode != newOut.Keymode {
+ return true
+ }
+ if len(oldOut.Tags) != len(newOut.Tags) {
+ return true
+ }
+ for i, newTag := range newOut.Tags {
+ if i >= len(oldOut.Tags) {
+ return true
+ }
+ oldTag := oldOut.Tags[i]
+ if oldTag.Tag != newTag.Tag || oldTag.State != newTag.State ||
+ oldTag.Clients != newTag.Clients || oldTag.Focused != newTag.Focused {
+ return true
+ }
+ }
+ }
+
+ return false
+}