diff options
| author | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
|---|---|---|
| committer | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
| commit | 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch) | |
| tree | ab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/dwl | |
| parent | 5d94c0a7d44a2255b81815a52a7056a94a39842d (diff) | |
| download | RaveOS-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')
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, ¤tState) { + 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 +} |