1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-27 15:02:50 -05:00

Compare commits

...

22 Commits

Author SHA1 Message Date
bbedward
4a68ce35a3 hyprland: remove focus grab from dankpopout 2025-11-16 11:44:59 -05:00
bbedward
eb4655fcbc rebase woes 2025-11-16 11:37:19 -05:00
bbedward
6eb349c9d4 barmask: fix multi-screen handling 2025-11-16 11:33:47 -05:00
bbedward
0a8a7895b3 hyprland: use FocusGrab for ondemand windows 2025-11-16 11:33:47 -05:00
bbedward
73c82a4dd9 dankbar/mask: extra polish for Hyprland maybe 2025-11-16 11:33:47 -05:00
bbedward
ccf28fc4e7 dankbar: add a mask while popouts are open
- Retains ability to click items on the bar, while another is open
2025-11-16 11:32:47 -05:00
bbedward
64ec5be919 wallpaper: empty input region 2025-11-15 23:41:24 -05:00
bbedward
3916512d66 systemtray: fix erroneous undefined condition 2025-11-15 21:46:34 -05:00
bbedward
e2f426a1bd Revert "systemtray: fix UI thread freeze when opening menu on Hyprland"
This reverts commit 4cb652abd9.
2025-11-15 21:42:50 -05:00
bbedward
aa1df8dfcf core: more syncmap conversions 2025-11-15 20:00:47 -05:00
bbedward
67557555f2 core: refactor to use a generic-compatible syncmap 2025-11-15 19:45:19 -05:00
bbedward
4cb652abd9 systemtray: fix UI thread freeze when opening menu on Hyprland
- Similar pattern as fix from Noctalia
2025-11-15 17:57:23 -05:00
bbedward
d11868b99f systray: don't try to force focus of menus 2025-11-15 14:57:47 -05:00
bbedward
1798417e6a systemtray: don't take keyboard focus
- bricks hyprland
2025-11-15 14:48:13 -05:00
github-actions[bot]
43dc3e5bb1 nix: update vendorHash for go.mod changes 2025-11-15 19:43:35 +00:00
bbedward
91891a14ed core/wayland: thread-safety meta fixes + cleanups + hypr workaround
- fork go-wayland/client and modify to make it thread-safe internally
- use sync.Map and atomic values in many places to cut down on mutex
  boilerplate
- do not create extworkspace client unless explicitly requested
2025-11-15 14:41:00 -05:00
bbedward
20f7d60147 settings: various consistency issues fixed
part of #725
2025-11-15 12:05:44 -05:00
bbedward
7e17e7d37a osd: fix opacity
part of #725
2025-11-15 11:43:05 -05:00
bbedward
cbb244f785 osd: add option to disable each OSD 2025-11-15 11:36:33 -05:00
Sunner
1c264d858b Follow symlinks when searching for sessions (#728) 2025-11-15 10:29:34 -05:00
bbedward
217037c2ae evdev: fix test 2025-11-14 23:26:14 -05:00
bbedward
b4dbd0b69c evdev: enhance keyboard detection for capslock 2025-11-14 23:22:06 -05:00
113 changed files with 10312 additions and 1270 deletions

View File

@@ -13,7 +13,6 @@ require (
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
github.com/spf13/cobra v1.10.1 github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
) )

View File

@@ -125,8 +125,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34 h1:iTAt1me6SBYsuzrl/CmrxtATPlOG/pVviosM3DhUdKE=
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34/go.mod h1:jzmUN5lUAv2O8e63OvcauV4S30rIZ1BvF/PNYE37vDo=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=

View File

@@ -1,12 +1,12 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/dwl-ipc-unstable-v2.xml // XML file : internal/proto/xml/dwl-ipc-unstable-v2.xml
// //
// dwl_ipc_unstable_v2 Protocol Copyright: // dwl_ipc_unstable_v2 Protocol Copyright:
package dwl_ipc package dwl_ipc
import "github.com/yaslama/go-wayland/wayland/client" import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
// ZdwlIpcManagerV2InterfaceName is the name of the interface as it appears in the [client.Registry]. // ZdwlIpcManagerV2InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the // It can be used to match the [client.RegistryGlobalEvent.Interface] in the

View File

@@ -1,5 +1,5 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : ext-workspace-v1.xml // XML file : ext-workspace-v1.xml
// //
// ext_workspace_v1 Protocol Copyright: // ext_workspace_v1 Protocol Copyright:
@@ -35,7 +35,8 @@ import (
"reflect" "reflect"
"unsafe" "unsafe"
"github.com/yaslama/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
// registerServerProxy registers a proxy with a server-assigned ID. // registerServerProxy registers a proxy with a server-assigned ID.
@@ -61,8 +62,9 @@ func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint3
return return
} }
objectsMap := reflect.NewAt(objectsField.Type(), unsafe.Pointer(objectsField.UnsafeAddr())).Elem() objectsMapPtr := unsafe.Pointer(objectsField.UnsafeAddr())
objectsMap.SetMapIndex(reflect.ValueOf(serverID), reflect.ValueOf(proxy)) objectsMap := (*syncmap.Map[uint32, client.Proxy])(objectsMapPtr)
objectsMap.Store(serverID, proxy)
} }
// ExtWorkspaceManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry]. // ExtWorkspaceManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].

View File

@@ -1,5 +1,5 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : wayland-protocols/wlr-gamma-control-unstable-v1.xml // XML file : wayland-protocols/wlr-gamma-control-unstable-v1.xml
// //
// wlr_gamma_control_unstable_v1 Protocol Copyright: // wlr_gamma_control_unstable_v1 Protocol Copyright:
@@ -31,7 +31,7 @@
package wlr_gamma_control package wlr_gamma_control
import ( import (
"github.com/yaslama/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )

View File

@@ -1,5 +1,5 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : /home/brandon/repos/dankdots/wlr-output-management-unstable-v1.xml // XML file : /home/brandon/repos/dankdots/wlr-output-management-unstable-v1.xml
// //
// wlr_output_management_unstable_v1 Protocol Copyright: // wlr_output_management_unstable_v1 Protocol Copyright:
@@ -33,7 +33,8 @@ import (
"reflect" "reflect"
"unsafe" "unsafe"
"github.com/yaslama/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint32) { func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint32) {
@@ -47,9 +48,9 @@ func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint3
if !objectsField.IsValid() { if !objectsField.IsValid() {
return return
} }
objectsField = reflect.NewAt(objectsField.Type(), unsafe.Pointer(objectsField.UnsafeAddr())).Elem() objectsMapPtr := unsafe.Pointer(objectsField.UnsafeAddr())
objectsMap := objectsField.Interface().(map[uint32]client.Proxy) objectsMap := (*syncmap.Map[uint32, client.Proxy])(objectsMapPtr)
objectsMap[serverID] = proxy objectsMap.Store(serverID, proxy)
} }
// ZwlrOutputManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry]. // ZwlrOutputManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].

View File

@@ -30,17 +30,13 @@ func NewManager() (*Manager, error) {
PairedDevices: []Device{}, PairedDevices: []Device{},
ConnectedDevices: []Device{}, ConnectedDevices: []Device{},
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan BluetoothState),
subMutex: sync.RWMutex{}, stopChan: make(chan struct{}),
stopChan: make(chan struct{}), dbusConn: conn,
dbusConn: conn, signals: make(chan *dbus.Signal, 256),
signals: make(chan *dbus.Signal, 256), dirty: make(chan struct{}, 1),
pairingSubscribers: make(map[string]chan PairingPrompt), eventQueue: make(chan func(), 32),
pairingSubMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
pendingPairings: make(map[string]bool),
eventQueue: make(chan func(), 32),
} }
broker := NewSubscriptionBroker(m.broadcastPairingPrompt) broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
@@ -360,12 +356,7 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
if hasPaired { if hasPaired {
if paired, ok := pairedVar.Value().(bool); ok && paired { if paired, ok := pairedVar.Value().(bool); ok && paired {
devicePath := string(path) devicePath := string(path)
m.pendingPairingsMux.Lock() _, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
wasPending := m.pendingPairings[devicePath]
if wasPending {
delete(m.pendingPairings, devicePath)
}
m.pendingPairingsMux.Unlock()
if wasPending { if wasPending {
select { select {
@@ -430,28 +421,20 @@ func (m *Manager) notifier() {
} }
m.updateDevices() m.updateDevices()
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan BluetoothState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -484,48 +467,36 @@ func (m *Manager) snapshotState() BluetoothState {
func (m *Manager) Subscribe(id string) chan BluetoothState { func (m *Manager) Subscribe(id string) chan BluetoothState {
ch := make(chan BluetoothState, 64) ch := make(chan BluetoothState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if ch, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok {
close(ch) close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) SubscribePairing(id string) chan PairingPrompt { func (m *Manager) SubscribePairing(id string) chan PairingPrompt {
ch := make(chan PairingPrompt, 16) ch := make(chan PairingPrompt, 16)
m.pairingSubMutex.Lock() m.pairingSubscribers.Store(id, ch)
m.pairingSubscribers[id] = ch
m.pairingSubMutex.Unlock()
return ch return ch
} }
func (m *Manager) UnsubscribePairing(id string) { func (m *Manager) UnsubscribePairing(id string) {
m.pairingSubMutex.Lock() if ch, ok := m.pairingSubscribers.LoadAndDelete(id); ok {
if ch, ok := m.pairingSubscribers[id]; ok {
close(ch) close(ch)
delete(m.pairingSubscribers, id)
} }
m.pairingSubMutex.Unlock()
} }
func (m *Manager) broadcastPairingPrompt(prompt PairingPrompt) { func (m *Manager) broadcastPairingPrompt(prompt PairingPrompt) {
m.pairingSubMutex.RLock() m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool {
defer m.pairingSubMutex.RUnlock()
for _, ch := range m.pairingSubscribers {
select { select {
case ch <- prompt: case ch <- prompt:
default: default:
} }
} return true
})
} }
func (m *Manager) SubmitPairing(token string, secrets map[string]string, accept bool) error { func (m *Manager) SubmitPairing(token string, secrets map[string]string, accept bool) error {
@@ -566,17 +537,13 @@ func (m *Manager) SetPowered(powered bool) error {
} }
func (m *Manager) PairDevice(devicePath string) error { func (m *Manager) PairDevice(devicePath string) error {
m.pendingPairingsMux.Lock() m.pendingPairings.Store(devicePath, true)
m.pendingPairings[devicePath] = true
m.pendingPairingsMux.Unlock()
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath)) obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath))
err := obj.Call(device1Iface+".Pair", 0).Err err := obj.Call(device1Iface+".Pair", 0).Err
if err != nil { if err != nil {
m.pendingPairingsMux.Lock() m.pendingPairings.Delete(devicePath)
delete(m.pendingPairings, devicePath)
m.pendingPairingsMux.Unlock()
} }
return err return err
@@ -618,19 +585,17 @@ func (m *Manager) Close() {
m.agent.Close() m.agent.Close()
} }
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan BluetoothState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan BluetoothState) return true
m.subMutex.Unlock() })
m.pairingSubMutex.Lock() m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool {
for _, ch := range m.pairingSubscribers {
close(ch) close(ch)
} m.pairingSubscribers.Delete(key)
m.pairingSubscribers = make(map[string]chan PairingPrompt) return true
m.pairingSubMutex.Unlock() })
if m.dbusConn != nil { if m.dbusConn != nil {
m.dbusConn.Close() m.dbusConn.Close()

View File

@@ -3,22 +3,19 @@ package bluez
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type SubscriptionBroker struct { type SubscriptionBroker struct {
mu sync.RWMutex pending syncmap.Map[string, chan PromptReply]
pending map[string]chan PromptReply requests syncmap.Map[string, PromptRequest]
requests map[string]PromptRequest
broadcastPrompt func(PairingPrompt) broadcastPrompt func(PairingPrompt)
} }
func NewSubscriptionBroker(broadcastPrompt func(PairingPrompt)) PromptBroker { func NewSubscriptionBroker(broadcastPrompt func(PairingPrompt)) PromptBroker {
return &SubscriptionBroker{ return &SubscriptionBroker{
pending: make(map[string]chan PromptReply),
requests: make(map[string]PromptRequest),
broadcastPrompt: broadcastPrompt, broadcastPrompt: broadcastPrompt,
} }
} }
@@ -30,10 +27,8 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
replyChan := make(chan PromptReply, 1) replyChan := make(chan PromptReply, 1)
b.mu.Lock() b.pending.Store(token, replyChan)
b.pending[token] = replyChan b.requests.Store(token, req)
b.requests[token] = req
b.mu.Unlock()
if b.broadcastPrompt != nil { if b.broadcastPrompt != nil {
prompt := PairingPrompt{ prompt := PairingPrompt{
@@ -53,10 +48,7 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) { func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
return PromptReply{}, fmt.Errorf("unknown token: %s", token) return PromptReply{}, fmt.Errorf("unknown token: %s", token)
} }
@@ -75,10 +67,7 @@ func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptRepl
} }
func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error { func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
return fmt.Errorf("unknown or expired token: %s", token) return fmt.Errorf("unknown or expired token: %s", token)
} }
@@ -92,8 +81,6 @@ func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
} }
func (b *SubscriptionBroker) cleanup(token string) { func (b *SubscriptionBroker) cleanup(token string) {
b.mu.Lock() b.pending.Delete(token)
delete(b.pending, token) b.requests.Delete(token)
delete(b.requests, token)
b.mu.Unlock()
} }

View File

@@ -3,6 +3,7 @@ package bluez
import ( import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -59,22 +60,19 @@ type PairingPrompt struct {
type Manager struct { type Manager struct {
state *BluetoothState state *BluetoothState
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan BluetoothState subscribers syncmap.Map[string, chan BluetoothState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
dbusConn *dbus.Conn dbusConn *dbus.Conn
signals chan *dbus.Signal signals chan *dbus.Signal
sigWG sync.WaitGroup sigWG sync.WaitGroup
agent *BluezAgent agent *BluezAgent
promptBroker PromptBroker promptBroker PromptBroker
pairingSubscribers map[string]chan PairingPrompt pairingSubscribers syncmap.Map[string, chan PairingPrompt]
pairingSubMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotifiedState *BluetoothState lastNotifiedState *BluetoothState
adapterPath dbus.ObjectPath adapterPath dbus.ObjectPath
pendingPairings map[string]bool pendingPairings syncmap.Map[string, bool]
pendingPairingsMux sync.Mutex
eventQueue chan func() eventQueue chan func()
eventWg sync.WaitGroup eventWg sync.WaitGroup
} }

View File

@@ -24,7 +24,6 @@ const (
func NewDDCBackend() (*DDCBackend, error) { func NewDDCBackend() (*DDCBackend, error) {
b := &DDCBackend{ b := &DDCBackend{
devices: make(map[string]*ddcDevice),
scanInterval: 30 * time.Second, scanInterval: 30 * time.Second,
debounceTimers: make(map[string]*time.Timer), debounceTimers: make(map[string]*time.Timer),
debouncePending: make(map[string]ddcPendingSet), debouncePending: make(map[string]ddcPendingSet),
@@ -53,10 +52,10 @@ func (b *DDCBackend) scanI2CDevices() error {
return nil return nil
} }
b.devicesMutex.Lock() b.devices.Range(func(key string, value *ddcDevice) bool {
defer b.devicesMutex.Unlock() b.devices.Delete(key)
return true
b.devices = make(map[string]*ddcDevice) })
for i := 0; i < 32; i++ { for i := 0; i < 32; i++ {
busPath := fmt.Sprintf("/dev/i2c-%d", i) busPath := fmt.Sprintf("/dev/i2c-%d", i)
@@ -64,7 +63,6 @@ func (b *DDCBackend) scanI2CDevices() error {
continue continue
} }
// Skip SMBus, GPU internal buses (e.g. AMDGPU SMU) to prevent GPU hangs
if isIgnorableI2CBus(i) { if isIgnorableI2CBus(i) {
log.Debugf("Skipping ignorable i2c-%d", i) log.Debugf("Skipping ignorable i2c-%d", i)
continue continue
@@ -77,7 +75,7 @@ func (b *DDCBackend) scanI2CDevices() error {
id := fmt.Sprintf("ddc:i2c-%d", i) id := fmt.Sprintf("ddc:i2c-%d", i)
dev.id = id dev.id = id
b.devices[id] = dev b.devices.Store(id, dev)
log.Debugf("found DDC device on i2c-%d", i) log.Debugf("found DDC device on i2c-%d", i)
} }
@@ -164,12 +162,9 @@ func (b *DDCBackend) GetDevices() ([]Device, error) {
log.Debugf("DDC scan error: %v", err) log.Debugf("DDC scan error: %v", err)
} }
b.devicesMutex.Lock() devices := make([]Device, 0)
defer b.devicesMutex.Unlock()
devices := make([]Device, 0, len(b.devices)) b.devices.Range(func(id string, dev *ddcDevice) bool {
for id, dev := range b.devices {
devices = append(devices, Device{ devices = append(devices, Device{
Class: ClassDDC, Class: ClassDDC,
ID: id, ID: id,
@@ -179,7 +174,8 @@ func (b *DDCBackend) GetDevices() ([]Device, error) {
CurrentPercent: dev.lastBrightness, CurrentPercent: dev.lastBrightness,
Backend: "ddc", Backend: "ddc",
}) })
} return true
})
return devices, nil return devices, nil
} }
@@ -189,9 +185,7 @@ func (b *DDCBackend) SetBrightness(id string, value int, exponential bool, callb
} }
func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error { func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error {
b.devicesMutex.RLock() _, ok := b.devices.Load(id)
_, ok := b.devices[id]
b.devicesMutex.RUnlock()
if !ok { if !ok {
return fmt.Errorf("device not found: %s", id) return fmt.Errorf("device not found: %s", id)
@@ -202,8 +196,6 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
} }
b.debounceMutex.Lock() b.debounceMutex.Lock()
defer b.debounceMutex.Unlock()
b.debouncePending[id] = ddcPendingSet{ b.debouncePending[id] = ddcPendingSet{
percent: value, percent: value,
callback: callback, callback: callback,
@@ -234,14 +226,13 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
} }
}) })
} }
b.debounceMutex.Unlock()
return nil return nil
} }
func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) error { func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) error {
b.devicesMutex.RLock() dev, ok := b.devices.Load(id)
dev, ok := b.devices[id]
b.devicesMutex.RUnlock()
if !ok { if !ok {
return fmt.Errorf("device not found: %s", id) return fmt.Errorf("device not found: %s", id)
@@ -266,9 +257,8 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
return fmt.Errorf("get current capability: %w", err) return fmt.Errorf("get current capability: %w", err)
} }
max = cap.max max = cap.max
b.devicesMutex.Lock()
dev.max = max dev.max = max
b.devicesMutex.Unlock() b.devices.Store(id, dev)
} }
if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, value); err != nil { if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, value); err != nil {
@@ -277,10 +267,9 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
log.Debugf("set %s to %d/%d", id, value, max) log.Debugf("set %s to %d/%d", id, value, max)
b.devicesMutex.Lock()
dev.max = max dev.max = max
dev.lastBrightness = value dev.lastBrightness = value
b.devicesMutex.Unlock() b.devices.Store(id, dev)
return nil return nil
} }

View File

@@ -15,10 +15,8 @@ func NewManager() (*Manager, error) {
func NewManagerWithOptions(exponential bool) (*Manager, error) { func NewManagerWithOptions(exponential bool) (*Manager, error) {
m := &Manager{ m := &Manager{
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate), exponential: exponential,
stopChan: make(chan struct{}),
exponential: exponential,
} }
go m.initLogind() go m.initLogind()
@@ -360,20 +358,13 @@ func (m *Manager) broadcastDeviceUpdate(deviceID string) {
update := DeviceUpdate{Device: *targetDevice} update := DeviceUpdate{Device: *targetDevice}
m.subMutex.RLock()
defer m.subMutex.RUnlock()
if len(m.updateSubscribers) == 0 {
log.Debugf("No update subscribers for device: %s", deviceID)
return
}
log.Debugf("Broadcasting device update: %s at %d%%", deviceID, targetDevice.CurrentPercent) log.Debugf("Broadcasting device update: %s at %d%%", deviceID, targetDevice.CurrentPercent)
for _, ch := range m.updateSubscribers { m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool {
select { select {
case ch <- update: case ch <- update:
default: default:
} }
} return true
})
} }

View File

@@ -13,9 +13,8 @@ import (
func NewSysfsBackend() (*SysfsBackend, error) { func NewSysfsBackend() (*SysfsBackend, error) {
b := &SysfsBackend{ b := &SysfsBackend{
basePath: "/sys/class", basePath: "/sys/class",
classes: []string{"backlight", "leds"}, classes: []string{"backlight", "leds"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := b.scanDevices(); err != nil { if err := b.scanDevices(); err != nil {
@@ -26,9 +25,6 @@ func NewSysfsBackend() (*SysfsBackend, error) {
} }
func (b *SysfsBackend) scanDevices() error { func (b *SysfsBackend) scanDevices() error {
b.deviceCacheMutex.Lock()
defer b.deviceCacheMutex.Unlock()
for _, class := range b.classes { for _, class := range b.classes {
classPath := filepath.Join(b.basePath, class) classPath := filepath.Join(b.basePath, class)
entries, err := os.ReadDir(classPath) entries, err := os.ReadDir(classPath)
@@ -68,13 +64,13 @@ func (b *SysfsBackend) scanDevices() error {
} }
deviceID := fmt.Sprintf("%s:%s", class, entry.Name()) deviceID := fmt.Sprintf("%s:%s", class, entry.Name())
b.deviceCache[deviceID] = &sysfsDevice{ b.deviceCache.Store(deviceID, &sysfsDevice{
class: deviceClass, class: deviceClass,
id: deviceID, id: deviceID,
name: entry.Name(), name: entry.Name(),
maxBrightness: maxBrightness, maxBrightness: maxBrightness,
minValue: minValue, minValue: minValue,
} })
log.Debugf("found %s device: %s (max=%d)", class, entry.Name(), maxBrightness) log.Debugf("found %s device: %s (max=%d)", class, entry.Name(), maxBrightness)
} }
@@ -106,19 +102,16 @@ func shouldSuppressDevice(name string) bool {
} }
func (b *SysfsBackend) GetDevices() ([]Device, error) { func (b *SysfsBackend) GetDevices() ([]Device, error) {
b.deviceCacheMutex.RLock() devices := make([]Device, 0)
defer b.deviceCacheMutex.RUnlock()
devices := make([]Device, 0, len(b.deviceCache)) b.deviceCache.Range(func(key string, dev *sysfsDevice) bool {
for _, dev := range b.deviceCache {
if shouldSuppressDevice(dev.name) { if shouldSuppressDevice(dev.name) {
continue return true
} }
parts := strings.SplitN(dev.id, ":", 2) parts := strings.SplitN(dev.id, ":", 2)
if len(parts) != 2 { if len(parts) != 2 {
continue return true
} }
class := parts[0] class := parts[0]
@@ -130,13 +123,13 @@ func (b *SysfsBackend) GetDevices() ([]Device, error) {
brightnessData, err := os.ReadFile(brightnessPath) brightnessData, err := os.ReadFile(brightnessPath)
if err != nil { if err != nil {
log.Debugf("failed to read brightness for %s: %v", dev.id, err) log.Debugf("failed to read brightness for %s: %v", dev.id, err)
continue return true
} }
current, err := strconv.Atoi(strings.TrimSpace(string(brightnessData))) current, err := strconv.Atoi(strings.TrimSpace(string(brightnessData)))
if err != nil { if err != nil {
log.Debugf("failed to parse brightness for %s: %v", dev.id, err) log.Debugf("failed to parse brightness for %s: %v", dev.id, err)
continue return true
} }
percent := b.ValueToPercent(current, dev, false) percent := b.ValueToPercent(current, dev, false)
@@ -150,16 +143,14 @@ func (b *SysfsBackend) GetDevices() ([]Device, error) {
CurrentPercent: percent, CurrentPercent: percent,
Backend: "sysfs", Backend: "sysfs",
}) })
} return true
})
return devices, nil return devices, nil
} }
func (b *SysfsBackend) GetDevice(id string) (*sysfsDevice, error) { func (b *SysfsBackend) GetDevice(id string) (*sysfsDevice, error) {
b.deviceCacheMutex.RLock() dev, ok := b.deviceCache.Load(id)
defer b.deviceCacheMutex.RUnlock()
dev, ok := b.deviceCache[id]
if !ok { if !ok {
return nil, fmt.Errorf("device not found: %s", id) return nil, fmt.Errorf("device not found: %s", id)
} }

View File

@@ -31,9 +31,8 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn) mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight"}, classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -41,13 +40,11 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
} }
m := &Manager{ m := &Manager{
logindBackend: mockLogind, logindBackend: mockLogind,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
} }
m.state = State{ m.state = State{
@@ -105,9 +102,8 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn) mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight"}, classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -115,13 +111,11 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
} }
m := &Manager{ m := &Manager{
logindBackend: mockLogind, logindBackend: mockLogind,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
} }
m.state = State{ m.state = State{
@@ -175,9 +169,8 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
} }
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight"}, classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -185,13 +178,11 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
} }
m := &Manager{ m := &Manager{
logindBackend: nil, logindBackend: nil,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: false, logindReady: false,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
} }
m.state = State{ m.state = State{
@@ -240,9 +231,8 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn) mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"leds"}, classes: []string{"leds"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -250,13 +240,11 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
} }
m := &Manager{ m := &Manager{
logindBackend: mockLogind, logindBackend: mockLogind,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
} }
m.state = State{ m.state = State{

View File

@@ -160,26 +160,21 @@ func TestSysfsBackend_ScanDevices(t *testing.T) {
} }
b := &SysfsBackend{ b := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight", "leds"}, classes: []string{"backlight", "leds"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := b.scanDevices(); err != nil { if err := b.scanDevices(); err != nil {
t.Fatalf("scanDevices() error = %v", err) t.Fatalf("scanDevices() error = %v", err)
} }
if len(b.deviceCache) != 2 {
t.Errorf("expected 2 devices, got %d", len(b.deviceCache))
}
backlightID := "backlight:test_backlight" backlightID := "backlight:test_backlight"
if _, ok := b.deviceCache[backlightID]; !ok { if _, ok := b.deviceCache.Load(backlightID); !ok {
t.Errorf("backlight device not found") t.Errorf("backlight device not found")
} }
ledID := "leds:test_led" ledID := "leds:test_led"
if _, ok := b.deviceCache[ledID]; !ok { if _, ok := b.deviceCache.Load(ledID); !ok {
t.Errorf("LED device not found") t.Errorf("LED device not found")
} }
} }

View File

@@ -3,6 +3,8 @@ package brightness
import ( import (
"sync" "sync"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type DeviceClass string type DeviceClass string
@@ -51,9 +53,8 @@ type Manager struct {
stateMutex sync.RWMutex stateMutex sync.RWMutex
state State state State
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
updateSubscribers map[string]chan DeviceUpdate updateSubscribers syncmap.Map[string, chan DeviceUpdate]
subMutex sync.RWMutex
broadcastMutex sync.Mutex broadcastMutex sync.Mutex
broadcastTimer *time.Timer broadcastTimer *time.Timer
@@ -67,8 +68,7 @@ type SysfsBackend struct {
basePath string basePath string
classes []string classes []string
deviceCache map[string]*sysfsDevice deviceCache syncmap.Map[string, *sysfsDevice]
deviceCacheMutex sync.RWMutex
} }
type sysfsDevice struct { type sysfsDevice struct {
@@ -80,8 +80,7 @@ type sysfsDevice struct {
} }
type DDCBackend struct { type DDCBackend struct {
devices map[string]*ddcDevice devices syncmap.Map[string, *ddcDevice]
devicesMutex sync.RWMutex
scanMutex sync.Mutex scanMutex sync.Mutex
lastScan time.Time lastScan time.Time
@@ -121,36 +120,31 @@ type SetBrightnessParams struct {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 16) ch := make(chan State, 16)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok { if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch) close(val)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) SubscribeUpdates(id string) chan DeviceUpdate { func (m *Manager) SubscribeUpdates(id string) chan DeviceUpdate {
ch := make(chan DeviceUpdate, 16) ch := make(chan DeviceUpdate, 16)
m.subMutex.Lock() m.updateSubscribers.Store(id, ch)
m.updateSubscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) UnsubscribeUpdates(id string) { func (m *Manager) UnsubscribeUpdates(id string) {
m.subMutex.Lock() if val, ok := m.updateSubscribers.LoadAndDelete(id); ok {
if ch, ok := m.updateSubscribers[id]; ok { close(val)
close(ch)
delete(m.updateSubscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) NotifySubscribers() { func (m *Manager) NotifySubscribers() {
@@ -158,15 +152,13 @@ func (m *Manager) NotifySubscribers() {
state := m.state state := m.state
m.stateMutex.RUnlock() m.stateMutex.RUnlock()
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
defer m.subMutex.RUnlock()
for _, ch := range m.subscribers {
select { select {
case ch <- state: case ch <- state:
default: default:
} }
} return true
})
} }
func (m *Manager) GetState() State { func (m *Manager) GetState() State {
@@ -178,16 +170,16 @@ func (m *Manager) GetState() State {
func (m *Manager) Close() { func (m *Manager) Close() {
close(m.stopChan) close(m.stopChan)
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
for _, ch := range m.updateSubscribers { })
m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool {
close(ch) close(ch)
} m.updateSubscribers.Delete(key)
m.updateSubscribers = make(map[string]chan DeviceUpdate) return true
m.subMutex.Unlock() })
if m.logindBackend != nil { if m.logindBackend != nil {
m.logindBackend.Close() m.logindBackend.Close()

View File

@@ -35,13 +35,11 @@ func NewManager() (*Manager, error) {
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: client, client: client,
baseURL: baseURL, baseURL: baseURL,
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
subMutex: sync.RWMutex{},
} }
if err := m.updateState(); err != nil { if err := m.updateState(); err != nil {
@@ -142,28 +140,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan CUPSState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -199,10 +190,14 @@ func (m *Manager) snapshotState() CUPSState {
func (m *Manager) Subscribe(id string) chan CUPSState { func (m *Manager) Subscribe(id string) chan CUPSState {
ch := make(chan CUPSState, 64) ch := make(chan CUPSState, 64)
m.subMutex.Lock()
wasEmpty := len(m.subscribers) == 0 wasEmpty := true
m.subscribers[id] = ch m.subscribers.Range(func(key string, ch chan CUPSState) bool {
m.subMutex.Unlock() wasEmpty = false
return false
})
m.subscribers.Store(id, ch)
if wasEmpty && m.subscription != nil { if wasEmpty && m.subscription != nil {
if err := m.subscription.Start(); err != nil { if err := m.subscription.Start(); err != nil {
@@ -217,13 +212,15 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
isEmpty := len(m.subscribers) == 0
m.subMutex.Unlock() isEmpty := true
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
isEmpty = false
return false
})
if isEmpty && m.subscription != nil { if isEmpty && m.subscription != nil {
m.subscription.Stop() m.subscription.Stop()
@@ -241,12 +238,11 @@ func (m *Manager) Close() {
m.eventWG.Wait() m.eventWG.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan CUPSState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan CUPSState) return true
m.subMutex.Unlock() })
} }
func stateChanged(old, new *CUPSState) bool { func stateChanged(old, new *CUPSState) bool {

View File

@@ -13,10 +13,9 @@ func TestNewManager(t *testing.T) {
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: nil, client: nil,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
assert.NotNil(t, m) assert.NotNil(t, m)
@@ -35,10 +34,9 @@ func TestManager_GetState(t *testing.T) {
}, },
}, },
}, },
client: mockClient, client: mockClient,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
state := m.GetState() state := m.GetState()
@@ -53,18 +51,28 @@ func TestManager_Subscribe(t *testing.T) {
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: mockClient, client: mockClient,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
ch := m.Subscribe("test-client") ch := m.Subscribe("test-client")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Equal(t, 1, len(m.subscribers))
count := 0
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
count++
return true
})
assert.Equal(t, 1, count)
m.Unsubscribe("test-client") m.Unsubscribe("test-client")
assert.Equal(t, 0, len(m.subscribers)) count = 0
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
count++
return true
})
assert.Equal(t, 0, count)
} }
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
@@ -74,10 +82,9 @@ func TestManager_Close(t *testing.T) {
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: mockClient, client: mockClient,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
m.eventWG.Add(1) m.eventWG.Add(1)
@@ -93,7 +100,12 @@ func TestManager_Close(t *testing.T) {
}() }()
m.Close() m.Close()
assert.Equal(t, 0, len(m.subscribers)) count := 0
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
count++
return true
})
assert.Equal(t, 0, count)
} }
func TestStateChanged(t *testing.T) { func TestStateChanged(t *testing.T) {

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type CUPSState struct { type CUPSState struct {
@@ -39,8 +40,7 @@ type Manager struct {
client CUPSClientInterface client CUPSClientInterface
subscription SubscriptionManagerInterface subscription SubscriptionManagerInterface
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan CUPSState subscribers syncmap.Map[string, chan CUPSState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
eventWG sync.WaitGroup eventWG sync.WaitGroup
dirty chan struct{} dirty chan struct{}

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"time" "time"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
@@ -14,13 +14,12 @@ func NewManager(display *wlclient.Display) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
ctx: display.Context(), ctx: display.Context(),
outputs: make(map[uint32]*outputState),
cmdq: make(chan cmd, 128), cmdq: make(chan cmd, 128),
outputSetupReq: make(chan uint32, 16), outputSetupReq: make(chan uint32, 16),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
layouts: make([]string, 0), layouts: make([]string, 0),
} }
if err := m.setupRegistry(); err != nil { if err := m.setupRegistry(); err != nil {
@@ -56,10 +55,7 @@ func (m *Manager) waylandActor() {
case c := <-m.cmdq: case c := <-m.cmdq:
c.fn() c.fn()
case outputID := <-m.outputSetupReq: case outputID := <-m.outputSetupReq:
m.outputsMutex.RLock() out, exists := m.outputs.Load(outputID)
out, exists := m.outputs[outputID]
m.outputsMutex.RUnlock()
if !exists { if !exists {
log.Warnf("DWL: Output %d no longer exists, skipping setup", outputID) log.Warnf("DWL: Output %d no longer exists, skipping setup", outputID)
continue continue
@@ -156,9 +152,7 @@ func (m *Manager) setupRegistry() error {
outputs = append(outputs, output) outputs = append(outputs, output)
outputRegNames[outputID] = e.Name outputRegNames[outputID] = e.Name
m.outputsMutex.Lock() m.outputs.Store(outputID, outState)
m.outputs[outputID] = outState
m.outputsMutex.Unlock()
if m.manager != nil { if m.manager != nil {
select { select {
@@ -176,17 +170,16 @@ func (m *Manager) setupRegistry() error {
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) { registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
m.post(func() { m.post(func() {
m.outputsMutex.Lock()
var outToRelease *outputState var outToRelease *outputState
for id, out := range m.outputs { m.outputs.Range(func(id uint32, out *outputState) bool {
if out.registryName == e.Name { if out.registryName == e.Name {
log.Infof("DWL: Output %d removed", id) log.Infof("DWL: Output %d removed", id)
outToRelease = out outToRelease = out
delete(m.outputs, id) m.outputs.Delete(id)
break return false
} }
} return true
m.outputsMutex.Unlock() })
if outToRelease != nil { if outToRelease != nil {
if ipcOut, ok := outToRelease.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok && ipcOut != nil { if ipcOut, ok := outToRelease.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok && ipcOut != nil {
@@ -236,14 +229,11 @@ func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclien
return fmt.Errorf("failed to get dwl output: %w", err) return fmt.Errorf("failed to get dwl output: %w", err)
} }
m.outputsMutex.Lock() outState, exists := m.outputs.Load(output.ID())
outState, exists := m.outputs[output.ID()]
if !exists { if !exists {
m.outputsMutex.Unlock()
return fmt.Errorf("output state not found for id %d", output.ID()) return fmt.Errorf("output state not found for id %d", output.ID())
} }
outState.ipcOutput = ipcOutput outState.ipcOutput = ipcOutput
m.outputsMutex.Unlock()
ipcOutput.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) { ipcOutput.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
outState.active = e.Active outState.active = e.Active
@@ -300,11 +290,10 @@ func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclien
} }
func (m *Manager) updateState() { func (m *Manager) updateState() {
m.outputsMutex.RLock()
outputs := make(map[string]*OutputState) outputs := make(map[string]*OutputState)
activeOutput := "" activeOutput := ""
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
@@ -326,8 +315,8 @@ func (m *Manager) updateState() {
if out.active != 0 { if out.active != 0 {
activeOutput = name activeOutput = name
} }
} return true
m.outputsMutex.RUnlock() })
newState := State{ newState := State{
Outputs: outputs, Outputs: outputs,
@@ -365,14 +354,6 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
subCount := len(m.subscribers)
m.subMutex.RUnlock()
if subCount == 0 {
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
@@ -381,15 +362,14 @@ func (m *Manager) notifier() {
continue continue
} }
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
log.Warn("DWL: subscriber channel full, dropping update") log.Warn("DWL: subscriber channel full, dropping update")
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -407,11 +387,9 @@ func (m *Manager) ensureOutputSetup(out *outputState) error {
} }
func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32) error { func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32) error {
m.outputsMutex.RLock() availableOutputs := make([]string, 0)
availableOutputs := make([]string, 0, len(m.outputs))
var targetOut *outputState var targetOut *outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
@@ -419,10 +397,10 @@ func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32
availableOutputs = append(availableOutputs, name) availableOutputs = append(availableOutputs, name)
if name == outputName { if name == outputName {
targetOut = out targetOut = out
break return false
} }
} return true
m.outputsMutex.RUnlock() })
if targetOut == nil { if targetOut == nil {
return fmt.Errorf("output not found: %s (available: %v)", outputName, availableOutputs) return fmt.Errorf("output not found: %s (available: %v)", outputName, availableOutputs)
@@ -444,20 +422,18 @@ func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32
} }
func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint32) error { func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint32) error {
m.outputsMutex.RLock()
var targetOut *outputState var targetOut *outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
} }
if name == outputName { if name == outputName {
targetOut = out targetOut = out
break return false
} }
} return true
m.outputsMutex.RUnlock() })
if targetOut == nil { if targetOut == nil {
return fmt.Errorf("output not found: %s", outputName) return fmt.Errorf("output not found: %s", outputName)
@@ -479,20 +455,18 @@ func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint3
} }
func (m *Manager) SetLayout(outputName string, index uint32) error { func (m *Manager) SetLayout(outputName string, index uint32) error {
m.outputsMutex.RLock()
var targetOut *outputState var targetOut *outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
} }
if name == outputName { if name == outputName {
targetOut = out targetOut = out
break return false
} }
} return true
m.outputsMutex.RUnlock() })
if targetOut == nil { if targetOut == nil {
return fmt.Errorf("output not found: %s", outputName) return fmt.Errorf("output not found: %s", outputName)
@@ -518,21 +492,19 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.outputsMutex.Lock() m.outputs.Range(func(key uint32, out *outputState) bool {
for _, out := range m.outputs {
if ipcOut, ok := out.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok { if ipcOut, ok := out.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok {
ipcOut.Release() ipcOut.Release()
} }
} m.outputs.Delete(key)
m.outputs = make(map[uint32]*outputState) return true
m.outputsMutex.Unlock() })
if mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2); ok { if mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2); ok {
mgr.Release() mgr.Release()

View File

@@ -3,7 +3,8 @@ package dwl
import ( import (
"sync" "sync"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type TagState struct { type TagState struct {
@@ -40,8 +41,7 @@ type Manager struct {
registry *wlclient.Registry registry *wlclient.Registry
manager interface{} manager interface{}
outputs map[uint32]*outputState outputs syncmap.Map[uint32, *outputState]
outputsMutex sync.RWMutex
tagCount uint32 tagCount uint32
layouts []string layouts []string
@@ -52,8 +52,7 @@ type Manager struct {
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -92,19 +91,16 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {

View File

@@ -47,10 +47,9 @@ func TestHandleRequest(t *testing.T) {
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{ m := &Manager{
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: true}, state: State{Available: true, CapsLock: true},
subscribers: make(map[string]chan State), closeChan: make(chan struct{}),
closeChan: make(chan struct{}),
} }
conn := newMockNetConn() conn := newMockNetConn()
@@ -77,10 +76,9 @@ func TestHandleRequest(t *testing.T) {
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{ m := &Manager{
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State), closeChan: make(chan struct{}),
closeChan: make(chan struct{}),
} }
conn := newMockNetConn() conn := newMockNetConn()
@@ -107,10 +105,9 @@ func TestHandleGetState(t *testing.T) {
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{ m := &Manager{
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State), closeChan: make(chan struct{}),
closeChan: make(chan struct{}),
} }
conn := newMockNetConn() conn := newMockNetConn()

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
evdev "github.com/holoplot/go-evdev" evdev "github.com/holoplot/go-evdev"
) )
@@ -35,8 +36,7 @@ type Manager struct {
monitoredPaths map[string]bool monitoredPaths map[string]bool
state State state State
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
closeChan chan struct{} closeChan chan struct{}
closeOnce sync.Once closeOnce sync.Once
watcher *fsnotify.Watcher watcher *fsnotify.Watcher
@@ -69,9 +69,9 @@ func NewManager() (*Manager, error) {
devices: devices, devices: devices,
monitoredPaths: monitoredPaths, monitoredPaths: monitoredPaths,
state: State{Available: true, CapsLock: initialCapsLock}, state: State{Available: true, CapsLock: initialCapsLock},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
watcher: watcher, watcher: watcher,
} }
for i, device := range devices { for i, device := range devices {
@@ -145,9 +145,18 @@ func isKeyboard(device EvdevDevice) bool {
return true return true
case strings.Contains(name, "input") && strings.Contains(name, "key"): case strings.Contains(name, "input") && strings.Contains(name, "key"):
return true return true
default: }
keyStates, err := device.State(evKeyType)
if err != nil {
return false return false
} }
hasKeyA := len(keyStates) > 30
hasKeyZ := len(keyStates) > 44
hasEnter := len(keyStates) > 28
return hasKeyA && hasKeyZ && hasEnter && len(keyStates) > 100
} }
func (m *Manager) watchForNewKeyboards() { func (m *Manager) watchForNewKeyboards() {
@@ -323,37 +332,25 @@ func (m *Manager) GetState() State {
} }
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
m.subMutex.Lock()
defer m.subMutex.Unlock()
ch := make(chan State, 16) ch := make(chan State, 16)
m.subscribers[id] = ch m.subscribers.Store(id, ch)
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
defer m.subMutex.Unlock() close(val)
ch, ok := m.subscribers[id]
if !ok {
return
} }
close(ch)
delete(m.subscribers, id)
} }
func (m *Manager) notifySubscribers(state State) { func (m *Manager) notifySubscribers(state State) {
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
defer m.subMutex.RUnlock()
for _, ch := range m.subscribers {
select { select {
case ch <- state: case ch <- state:
default: default:
} }
} return true
})
} }
func (m *Manager) Close() { func (m *Manager) Close() {
@@ -375,12 +372,11 @@ func (m *Manager) Close() {
} }
m.devicesMutex.Unlock() m.devicesMutex.Unlock()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for id, ch := range m.subscribers {
close(ch) close(ch)
delete(m.subscribers, id) m.subscribers.Delete(key)
} return true
m.subMutex.Unlock() })
}) })
} }

View File

@@ -16,10 +16,9 @@ func TestManager_Creation(t *testing.T) {
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{ m := &Manager{
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State), closeChan: make(chan struct{}),
closeChan: make(chan struct{}),
} }
assert.NotNil(t, m) assert.NotNil(t, m)
@@ -32,10 +31,9 @@ func TestManager_Creation(t *testing.T) {
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{ m := &Manager{
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: true}, state: State{Available: true, CapsLock: true},
subscribers: make(map[string]chan State), closeChan: make(chan struct{}),
closeChan: make(chan struct{}),
} }
assert.NotNil(t, m) assert.NotNil(t, m)
@@ -52,7 +50,6 @@ func TestManager_GetState(t *testing.T) {
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool), monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
@@ -69,13 +66,17 @@ func TestManager_Subscribe(t *testing.T) {
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool), monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
ch := m.Subscribe("test-client") ch := m.Subscribe("test-client")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Len(t, m.subscribers, 1) count := 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 1, count)
} }
func TestManager_Unsubscribe(t *testing.T) { func TestManager_Unsubscribe(t *testing.T) {
@@ -86,15 +87,24 @@ func TestManager_Unsubscribe(t *testing.T) {
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool), monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
ch := m.Subscribe("test-client") ch := m.Subscribe("test-client")
assert.Len(t, m.subscribers, 1) count := 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 1, count)
m.Unsubscribe("test-client") m.Unsubscribe("test-client")
assert.Len(t, m.subscribers, 0) count = 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 0, count)
select { select {
case _, ok := <-ch: case _, ok := <-ch:
@@ -112,7 +122,6 @@ func TestManager_UpdateCapsLock(t *testing.T) {
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool), monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
@@ -148,7 +157,6 @@ func TestManager_Close(t *testing.T) {
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool), monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
@@ -171,7 +179,12 @@ func TestManager_Close(t *testing.T) {
t.Error("channel 2 should be closed") t.Error("channel 2 should be closed")
} }
assert.Len(t, m.subscribers, 0) count := 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 0, count)
m.Close() m.Close()
} }
@@ -194,6 +207,10 @@ func TestIsKeyboard(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t) mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().Name().Return(tt.devName, nil).Once() mockDevice.EXPECT().Name().Return(tt.devName, nil).Once()
if !tt.expected {
mockDevice.EXPECT().State(evdev.EvType(evKeyType)).Return(evdev.StateMap{}, nil).Maybe()
}
result := isKeyboard(mockDevice) result := isKeyboard(mockDevice)
assert.Equal(t, tt.expected, result) assert.Equal(t, tt.expected, result)
}) })
@@ -226,10 +243,9 @@ func TestManager_MonitorDevice(t *testing.T) {
mockDevice.EXPECT().Close().Return(nil).Maybe() mockDevice.EXPECT().Close().Return(nil).Maybe()
m := &Manager{ m := &Manager{
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State), closeChan: make(chan struct{}),
closeChan: make(chan struct{}),
} }
ch := m.Subscribe("test") ch := m.Subscribe("test")
@@ -272,7 +288,6 @@ func TestNotifySubscribers(t *testing.T) {
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool), monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }

View File

@@ -6,21 +6,17 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
func NewManager(display *wlclient.Display) (*Manager, error) { func NewManager(display *wlclient.Display) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
ctx: display.Context(), ctx: display.Context(),
outputs: make(map[uint32]*wlclient.Output), cmdq: make(chan cmd, 128),
outputNames: make(map[uint32]string), stopChan: make(chan struct{}),
groups: make(map[uint32]*workspaceGroupState),
workspaces: make(map[uint32]*workspaceState), dirty: make(chan struct{}, 1),
cmdq: make(chan cmd, 128),
stopChan: make(chan struct{}),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1),
} }
m.wg.Add(1) m.wg.Add(1)
@@ -77,9 +73,7 @@ func (m *Manager) setupRegistry() error {
outputID := output.ID() outputID := output.ID()
output.SetNameHandler(func(ev wlclient.OutputNameEvent) { output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
m.outputsMutex.Lock() m.outputNames.Store(outputID, ev.Name)
m.outputNames[outputID] = ev.Name
m.outputsMutex.Unlock()
log.Debugf("ExtWorkspace: Output %d (%s) name received", outputID, ev.Name) log.Debugf("ExtWorkspace: Output %d (%s) name received", outputID, ev.Name)
}) })
} }
@@ -139,9 +133,7 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
workspaceIDs: make([]uint32, 0), workspaceIDs: make([]uint32, 0),
} }
m.groupsMutex.Lock() m.groups.Store(groupID, group)
m.groups[groupID] = group
m.groupsMutex.Unlock()
handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1CapabilitiesEvent) { handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1CapabilitiesEvent) {
log.Debugf("ExtWorkspace: Group %d capabilities: %d", groupID, e.Capabilities) log.Debugf("ExtWorkspace: Group %d capabilities: %d", groupID, e.Capabilities)
@@ -171,11 +163,9 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
log.Debugf("ExtWorkspace: Group %d workspace enter (workspace=%d)", groupID, workspaceID) log.Debugf("ExtWorkspace: Group %d workspace enter (workspace=%d)", groupID, workspaceID)
m.post(func() { m.post(func() {
m.workspacesMutex.Lock() if ws, ok := m.workspaces.Load(workspaceID); ok {
if ws, exists := m.workspaces[workspaceID]; exists {
ws.groupID = groupID ws.groupID = groupID
} }
m.workspacesMutex.Unlock()
group.workspaceIDs = append(group.workspaceIDs, workspaceID) group.workspaceIDs = append(group.workspaceIDs, workspaceID)
m.updateState() m.updateState()
@@ -187,11 +177,9 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
log.Debugf("ExtWorkspace: Group %d workspace leave (workspace=%d)", groupID, workspaceID) log.Debugf("ExtWorkspace: Group %d workspace leave (workspace=%d)", groupID, workspaceID)
m.post(func() { m.post(func() {
m.workspacesMutex.Lock() if ws, ok := m.workspaces.Load(workspaceID); ok {
if ws, exists := m.workspaces[workspaceID]; exists {
ws.groupID = 0 ws.groupID = 0
} }
m.workspacesMutex.Unlock()
for i, id := range group.workspaceIDs { for i, id := range group.workspaceIDs {
if id == workspaceID { if id == workspaceID {
@@ -209,9 +197,7 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
m.post(func() { m.post(func() {
group.removed = true group.removed = true
m.groupsMutex.Lock() m.groups.Delete(groupID)
delete(m.groups, groupID)
m.groupsMutex.Unlock()
m.wlMutex.Lock() m.wlMutex.Lock()
handle.Destroy() handle.Destroy()
@@ -234,9 +220,7 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
coordinates: make([]uint32, 0), coordinates: make([]uint32, 0),
} }
m.workspacesMutex.Lock() m.workspaces.Store(workspaceID, ws)
m.workspaces[workspaceID] = ws
m.workspacesMutex.Unlock()
handle.SetIdHandler(func(e ext_workspace.ExtWorkspaceHandleV1IdEvent) { handle.SetIdHandler(func(e ext_workspace.ExtWorkspaceHandleV1IdEvent) {
log.Debugf("ExtWorkspace: Workspace %d id: %s", workspaceID, e.Id) log.Debugf("ExtWorkspace: Workspace %d id: %s", workspaceID, e.Id)
@@ -290,9 +274,7 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
m.post(func() { m.post(func() {
ws.removed = true ws.removed = true
m.workspacesMutex.Lock() m.workspaces.Delete(workspaceID)
delete(m.workspaces, workspaceID)
m.workspacesMutex.Unlock()
m.wlMutex.Lock() m.wlMutex.Lock()
handle.Destroy() handle.Destroy()
@@ -304,23 +286,21 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
} }
func (m *Manager) updateState() { func (m *Manager) updateState() {
m.groupsMutex.RLock()
m.workspacesMutex.RLock()
groups := make([]*WorkspaceGroup, 0) groups := make([]*WorkspaceGroup, 0)
for _, group := range m.groups { m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
if group.removed { if group.removed {
continue return true
} }
outputs := make([]string, 0) outputs := make([]string, 0)
for outputID := range group.outputIDs { for outputID := range group.outputIDs {
m.outputsMutex.RLock() if name, ok := m.outputNames.Load(outputID); ok {
name := m.outputNames[outputID] if name != "" {
m.outputsMutex.RUnlock() outputs = append(outputs, name)
if name != "" { } else {
outputs = append(outputs, name) outputs = append(outputs, fmt.Sprintf("output-%d", outputID))
}
} else { } else {
outputs = append(outputs, fmt.Sprintf("output-%d", outputID)) outputs = append(outputs, fmt.Sprintf("output-%d", outputID))
} }
@@ -328,8 +308,11 @@ func (m *Manager) updateState() {
workspaces := make([]*Workspace, 0) workspaces := make([]*Workspace, 0)
for _, wsID := range group.workspaceIDs { for _, wsID := range group.workspaceIDs {
ws, exists := m.workspaces[wsID] ws, exists := m.workspaces.Load(wsID)
if !exists || ws.removed { if !exists {
continue
}
if ws.removed {
continue continue
} }
@@ -351,10 +334,8 @@ func (m *Manager) updateState() {
Workspaces: workspaces, Workspaces: workspaces,
} }
groups = append(groups, groupState) groups = append(groups, groupState)
} return true
})
m.workspacesMutex.RUnlock()
m.groupsMutex.RUnlock()
newState := State{ newState := State{
Groups: groups, Groups: groups,
@@ -389,14 +370,6 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
subCount := len(m.subscribers)
m.subMutex.RUnlock()
if subCount == 0 {
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
@@ -405,15 +378,14 @@ func (m *Manager) notifier() {
continue continue
} }
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
log.Warn("ExtWorkspace: subscriber channel full, dropping update") log.Warn("ExtWorkspace: subscriber channel full, dropping update")
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -426,9 +398,6 @@ func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error {
errChan := make(chan error, 1) errChan := make(chan error, 1)
m.post(func() { m.post(func() {
m.workspacesMutex.RLock()
defer m.workspacesMutex.RUnlock()
var targetGroupID uint32 var targetGroupID uint32
if groupID != "" { if groupID != "" {
var parsedID uint32 var parsedID uint32
@@ -437,9 +406,10 @@ func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error {
} }
} }
for _, ws := range m.workspaces { var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID { if targetGroupID != 0 && ws.groupID != targetGroupID {
continue return true
} }
if ws.workspaceID == workspaceID || ws.name == workspaceID { if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -449,11 +419,15 @@ func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error {
} }
m.wlMutex.Unlock() m.wlMutex.Unlock()
errChan <- err errChan <- err
return found = true
return false
} }
} return true
})
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
}) })
return <-errChan return <-errChan
@@ -463,9 +437,6 @@ func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error {
errChan := make(chan error, 1) errChan := make(chan error, 1)
m.post(func() { m.post(func() {
m.workspacesMutex.RLock()
defer m.workspacesMutex.RUnlock()
var targetGroupID uint32 var targetGroupID uint32
if groupID != "" { if groupID != "" {
var parsedID uint32 var parsedID uint32
@@ -474,9 +445,10 @@ func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error {
} }
} }
for _, ws := range m.workspaces { var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID { if targetGroupID != 0 && ws.groupID != targetGroupID {
continue return true
} }
if ws.workspaceID == workspaceID || ws.name == workspaceID { if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -486,11 +458,15 @@ func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error {
} }
m.wlMutex.Unlock() m.wlMutex.Unlock()
errChan <- err errChan <- err
return found = true
return false
} }
} return true
})
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
}) })
return <-errChan return <-errChan
@@ -500,9 +476,6 @@ func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error {
errChan := make(chan error, 1) errChan := make(chan error, 1)
m.post(func() { m.post(func() {
m.workspacesMutex.RLock()
defer m.workspacesMutex.RUnlock()
var targetGroupID uint32 var targetGroupID uint32
if groupID != "" { if groupID != "" {
var parsedID uint32 var parsedID uint32
@@ -511,9 +484,10 @@ func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error {
} }
} }
for _, ws := range m.workspaces { var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID { if targetGroupID != 0 && ws.groupID != targetGroupID {
continue return true
} }
if ws.workspaceID == workspaceID || ws.name == workspaceID { if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -523,11 +497,15 @@ func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error {
} }
m.wlMutex.Unlock() m.wlMutex.Unlock()
errChan <- err errChan <- err
return found = true
return false
} }
} return true
})
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
}) })
return <-errChan return <-errChan
@@ -537,10 +515,8 @@ func (m *Manager) CreateWorkspace(groupID, workspaceName string) error {
errChan := make(chan error, 1) errChan := make(chan error, 1)
m.post(func() { m.post(func() {
m.groupsMutex.RLock() var found bool
defer m.groupsMutex.RUnlock() m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
for _, group := range m.groups {
if fmt.Sprintf("group-%d", group.id) == groupID { if fmt.Sprintf("group-%d", group.id) == groupID {
m.wlMutex.Lock() m.wlMutex.Lock()
err := group.handle.CreateWorkspace(workspaceName) err := group.handle.CreateWorkspace(workspaceName)
@@ -549,11 +525,15 @@ func (m *Manager) CreateWorkspace(groupID, workspaceName string) error {
} }
m.wlMutex.Unlock() m.wlMutex.Unlock()
errChan <- err errChan <- err
return found = true
return false
} }
} return true
})
errChan <- fmt.Errorf("workspace group not found: %s", groupID) if !found {
errChan <- fmt.Errorf("workspace group not found: %s", groupID)
}
}) })
return <-errChan return <-errChan
@@ -564,30 +544,27 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.workspacesMutex.Lock() m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
for _, ws := range m.workspaces {
if ws.handle != nil { if ws.handle != nil {
ws.handle.Destroy() ws.handle.Destroy()
} }
} m.workspaces.Delete(key)
m.workspaces = make(map[uint32]*workspaceState) return true
m.workspacesMutex.Unlock() })
m.groupsMutex.Lock() m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
for _, group := range m.groups {
if group.handle != nil { if group.handle != nil {
group.handle.Destroy() group.handle.Destroy()
} }
} m.groups.Delete(key)
m.groups = make(map[uint32]*workspaceGroupState) return true
m.groupsMutex.Unlock() })
if m.manager != nil { if m.manager != nil {
m.manager.Stop() m.manager.Stop()

View File

@@ -4,7 +4,8 @@ import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type Workspace struct { type Workspace struct {
@@ -37,23 +38,18 @@ type Manager struct {
registry *wlclient.Registry registry *wlclient.Registry
manager *ext_workspace.ExtWorkspaceManagerV1 manager *ext_workspace.ExtWorkspaceManagerV1
outputsMutex sync.RWMutex outputNames syncmap.Map[uint32, string]
outputs map[uint32]*wlclient.Output
outputNames map[uint32]string
groupsMutex sync.RWMutex groups syncmap.Map[uint32, *workspaceGroupState]
groups map[uint32]*workspaceGroupState
workspacesMutex sync.RWMutex workspaces syncmap.Map[uint32, *workspaceState]
workspaces map[uint32]*workspaceState
wlMutex sync.Mutex wlMutex sync.Mutex
cmdq chan cmd cmdq chan cmd
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -95,19 +91,16 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if ch, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok {
close(ch) close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {

View File

@@ -29,8 +29,6 @@ func NewManager() (*Manager, error) {
systemConn: systemConn, systemConn: systemConn,
sessionConn: sessionConn, sessionConn: sessionConn,
currentUID: uint64(os.Getuid()), currentUID: uint64(os.Getuid()),
subscribers: make(map[string]chan FreedeskState),
subMutex: sync.RWMutex{},
} }
m.initializeAccounts() m.initializeAccounts()
@@ -206,41 +204,33 @@ func (m *Manager) GetState() FreedeskState {
func (m *Manager) Subscribe(id string) chan FreedeskState { func (m *Manager) Subscribe(id string) chan FreedeskState {
ch := make(chan FreedeskState, 64) ch := make(chan FreedeskState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) NotifySubscribers() { func (m *Manager) NotifySubscribers() {
m.subMutex.RLock()
defer m.subMutex.RUnlock()
state := m.GetState() state := m.GetState()
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan FreedeskState) bool {
select { select {
case ch <- state: case ch <- state:
default: default:
} }
} return true
})
} }
func (m *Manager) Close() { func (m *Manager) Close() {
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan FreedeskState) bool {
for id, ch := range m.subscribers {
close(ch) close(ch)
delete(m.subscribers, id) m.subscribers.Delete(key)
} return true
m.subMutex.Unlock() })
if m.systemConn != nil { if m.systemConn != nil {
m.systemConn.Close() m.systemConn.Close()

View File

@@ -3,6 +3,7 @@ package freedesktop
import ( import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -41,6 +42,5 @@ type Manager struct {
accountsObj dbus.BusObject accountsObj dbus.BusObject
settingsObj dbus.BusObject settingsObj dbus.BusObject
currentUID uint64 currentUID uint64
subscribers map[string]chan FreedeskState subscribers syncmap.Map[string, chan FreedeskState]
subMutex sync.RWMutex
} }

View File

@@ -466,9 +466,7 @@ func TestHandleSubscribe(t *testing.T) {
SessionID: "1", SessionID: "1",
Locked: false, Locked: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{},
} }
conn := newMockNetConn() conn := newMockNetConn()

View File

@@ -25,13 +25,12 @@ func NewManager() (*Manager, error) {
state: &SessionState{ state: &SessionState{
SessionID: sessionID, SessionID: sessionID,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{}, stopChan: make(chan struct{}),
stopChan: make(chan struct{}), conn: conn,
conn: conn, dirty: make(chan struct{}, 1),
dirty: make(chan struct{}, 1), signals: make(chan *dbus.Signal, 256),
signals: make(chan *dbus.Signal, 256),
} }
m.sleepInhibitorEnabled.Store(true) m.sleepInhibitorEnabled.Store(true)
@@ -351,19 +350,14 @@ func (m *Manager) GetState() SessionState {
func (m *Manager) Subscribe(id string) chan SessionState { func (m *Manager) Subscribe(id string) chan SessionState {
ch := make(chan SessionState, 64) ch := make(chan SessionState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifier() { func (m *Manager) notifier() {
@@ -387,28 +381,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan SessionState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -584,12 +571,11 @@ func (m *Manager) Close() {
m.releaseSleepInhibitor() m.releaseSleepInhibitor()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan SessionState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan SessionState) return true
m.subMutex.Unlock() })
if m.conn != nil { if m.conn != nil {
m.conn.Close() m.conn.Close()

View File

@@ -34,26 +34,20 @@ func TestManager_GetState(t *testing.T) {
func TestManager_Subscribe(t *testing.T) { func TestManager_Subscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{},
} }
ch := manager.Subscribe("test-client") ch := manager.Subscribe("test-client")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Equal(t, 64, cap(ch)) assert.Equal(t, 64, cap(ch))
manager.subMutex.RLock() _, exists := manager.subscribers.Load("test-client")
_, exists := manager.subscribers["test-client"]
manager.subMutex.RUnlock()
assert.True(t, exists) assert.True(t, exists)
} }
func TestManager_Unsubscribe(t *testing.T) { func TestManager_Unsubscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{},
} }
ch := manager.Subscribe("test-client") ch := manager.Subscribe("test-client")
@@ -63,17 +57,13 @@ func TestManager_Unsubscribe(t *testing.T) {
_, ok := <-ch _, ok := <-ch
assert.False(t, ok) assert.False(t, ok)
manager.subMutex.RLock() _, exists := manager.subscribers.Load("test-client")
_, exists := manager.subscribers["test-client"]
manager.subMutex.RUnlock()
assert.False(t, exists) assert.False(t, exists)
} }
func TestManager_Unsubscribe_NonExistent(t *testing.T) { func TestManager_Unsubscribe_NonExistent(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{},
} }
// Unsubscribe a non-existent client should not panic // Unsubscribe a non-existent client should not panic
@@ -88,19 +78,15 @@ func TestManager_NotifySubscribers(t *testing.T) {
SessionID: "1", SessionID: "1",
Locked: false, Locked: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{}, dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
} }
manager.notifierWg.Add(1) manager.notifierWg.Add(1)
go manager.notifier() go manager.notifier()
ch := make(chan SessionState, 10) ch := make(chan SessionState, 10)
manager.subMutex.Lock() manager.subscribers.Store("test-client", ch)
manager.subscribers["test-client"] = ch
manager.subMutex.Unlock()
manager.notifySubscribers() manager.notifySubscribers()
@@ -122,19 +108,15 @@ func TestManager_NotifySubscribers_Debounce(t *testing.T) {
SessionID: "1", SessionID: "1",
Locked: false, Locked: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{}, dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
} }
manager.notifierWg.Add(1) manager.notifierWg.Add(1)
go manager.notifier() go manager.notifier()
ch := make(chan SessionState, 10) ch := make(chan SessionState, 10)
manager.subMutex.Lock() manager.subscribers.Store("test-client", ch)
manager.subscribers["test-client"] = ch
manager.subMutex.Unlock()
manager.notifySubscribers() manager.notifySubscribers()
manager.notifySubscribers() manager.notifySubscribers()
@@ -157,19 +139,15 @@ func TestManager_NotifySubscribers_Debounce(t *testing.T) {
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
} }
ch1 := make(chan SessionState, 1) ch1 := make(chan SessionState, 1)
ch2 := make(chan SessionState, 1) ch2 := make(chan SessionState, 1)
manager.subMutex.Lock() manager.subscribers.Store("client1", ch1)
manager.subscribers["client1"] = ch1 manager.subscribers.Store("client2", ch2)
manager.subscribers["client2"] = ch2
manager.subMutex.Unlock()
manager.Close() manager.Close()
@@ -184,7 +162,12 @@ func TestManager_Close(t *testing.T) {
assert.False(t, ok1, "ch1 should be closed") assert.False(t, ok1, "ch1 should be closed")
assert.False(t, ok2, "ch2 should be closed") assert.False(t, ok2, "ch2 should be closed")
assert.Len(t, manager.subscribers, 0) count := 0
manager.subscribers.Range(func(key string, ch chan SessionState) bool {
count++
return true
})
assert.Equal(t, 0, count)
} }
func TestManager_GetState_ThreadSafe(t *testing.T) { func TestManager_GetState_ThreadSafe(t *testing.T) {

View File

@@ -14,10 +14,8 @@ func TestManager_HandleDBusSignal_Lock(t *testing.T) {
Locked: false, Locked: false,
LockedHint: false, LockedHint: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -38,10 +36,8 @@ func TestManager_HandleDBusSignal_Unlock(t *testing.T) {
Locked: true, Locked: true,
LockedHint: true, LockedHint: true,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -62,10 +58,8 @@ func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) {
state: &SessionState{ state: &SessionState{
PreparingForSleep: false, PreparingForSleep: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -85,10 +79,8 @@ func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) {
state: &SessionState{ state: &SessionState{
PreparingForSleep: true, PreparingForSleep: true,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -108,10 +100,8 @@ func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) {
state: &SessionState{ state: &SessionState{
PreparingForSleep: false, PreparingForSleep: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -133,10 +123,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
state: &SessionState{ state: &SessionState{
Active: false, Active: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -161,10 +149,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
state: &SessionState{ state: &SessionState{
IdleHint: false, IdleHint: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -189,10 +175,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
state: &SessionState{ state: &SessionState{
IdleSinceHint: 0, IdleSinceHint: 0,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -218,10 +202,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
LockedHint: false, LockedHint: false,
Locked: false, Locked: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -247,10 +229,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
state: &SessionState{ state: &SessionState{
Active: false, Active: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -272,11 +252,9 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
t.Run("empty body", func(t *testing.T) { t.Run("empty body", func(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -295,10 +273,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
Active: false, Active: false,
IdleHint: false, IdleHint: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{

View File

@@ -6,6 +6,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -50,8 +51,7 @@ type SessionEvent struct {
type Manager struct { type Manager struct {
state *SessionState state *SessionState
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan SessionState subscribers syncmap.Map[string, chan SessionState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
conn *dbus.Conn conn *dbus.Conn
sessionPath dbus.ObjectPath sessionPath dbus.ObjectPath

View File

@@ -240,19 +240,25 @@ func TestHandleSubscribe(t *testing.T) {
func TestManager_Subscribe_Unsubscribe(t *testing.T) { func TestManager_Subscribe_Unsubscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &NetworkState{}, state: &NetworkState{},
subscribers: make(map[string]chan NetworkState),
} }
t.Run("subscribe creates channel", func(t *testing.T) { t.Run("subscribe creates channel", func(t *testing.T) {
ch := manager.Subscribe("client1") ch := manager.Subscribe("client1")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Len(t, manager.subscribers, 1) count := 0
manager.subscribers.Range(func(key string, ch chan NetworkState) bool {
count++
return true
})
assert.Equal(t, 1, count)
}) })
t.Run("unsubscribe removes channel", func(t *testing.T) { t.Run("unsubscribe removes channel", func(t *testing.T) {
manager.Unsubscribe("client1") manager.Unsubscribe("client1")
assert.Len(t, manager.subscribers, 0) count := 0
manager.subscribers.Range(func(key string, ch chan NetworkState) bool { count++; return true })
assert.Equal(t, 0, count)
}) })
t.Run("unsubscribe non-existent client is safe", func(t *testing.T) { t.Run("unsubscribe non-existent client is safe", func(t *testing.T) {

View File

@@ -66,13 +66,10 @@ func NewManager() (*Manager, error) {
Preference: PreferenceAuto, Preference: PreferenceAuto,
WiFiNetworks: []WiFiNetwork{}, WiFiNetworks: []WiFiNetwork{},
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan NetworkState),
subMutex: sync.RWMutex{}, stopChan: make(chan struct{}),
stopChan: make(chan struct{}), dirty: make(chan struct{}, 1),
dirty: make(chan struct{}, 1),
credentialSubscribers: make(map[string]chan CredentialPrompt),
credSubMutex: sync.RWMutex{},
} }
broker := NewSubscriptionBroker(m.broadcastCredentialPrompt) broker := NewSubscriptionBroker(m.broadcastCredentialPrompt)
@@ -270,48 +267,36 @@ func (m *Manager) GetState() NetworkState {
func (m *Manager) Subscribe(id string) chan NetworkState { func (m *Manager) Subscribe(id string) chan NetworkState {
ch := make(chan NetworkState, 64) ch := make(chan NetworkState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) SubscribeCredentials(id string) chan CredentialPrompt { func (m *Manager) SubscribeCredentials(id string) chan CredentialPrompt {
ch := make(chan CredentialPrompt, 16) ch := make(chan CredentialPrompt, 16)
m.credSubMutex.Lock() m.credentialSubscribers.Store(id, ch)
m.credentialSubscribers[id] = ch
m.credSubMutex.Unlock()
return ch return ch
} }
func (m *Manager) UnsubscribeCredentials(id string) { func (m *Manager) UnsubscribeCredentials(id string) {
m.credSubMutex.Lock() if ch, ok := m.credentialSubscribers.LoadAndDelete(id); ok {
if ch, ok := m.credentialSubscribers[id]; ok {
close(ch) close(ch)
delete(m.credentialSubscribers, id)
} }
m.credSubMutex.Unlock()
} }
func (m *Manager) broadcastCredentialPrompt(prompt CredentialPrompt) { func (m *Manager) broadcastCredentialPrompt(prompt CredentialPrompt) {
m.credSubMutex.RLock() m.credentialSubscribers.Range(func(key string, ch chan CredentialPrompt) bool {
defer m.credSubMutex.RUnlock()
for _, ch := range m.credentialSubscribers {
select { select {
case ch <- prompt: case ch <- prompt:
default: default:
} }
} return true
})
} }
func (m *Manager) notifier() { func (m *Manager) notifier() {
@@ -335,28 +320,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan NetworkState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -396,12 +374,11 @@ func (m *Manager) Close() {
m.backend.Close() m.backend.Close()
} }
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan NetworkState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan NetworkState) return true
m.subMutex.Unlock() })
} }
func (m *Manager) ScanWiFi() error { func (m *Manager) ScanWiFi() error {

View File

@@ -31,19 +31,15 @@ func TestManager_NotifySubscribers(t *testing.T) {
state: &NetworkState{ state: &NetworkState{
NetworkStatus: StatusWiFi, NetworkStatus: StatusWiFi,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan NetworkState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{}, dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
} }
manager.notifierWg.Add(1) manager.notifierWg.Add(1)
go manager.notifier() go manager.notifier()
ch := make(chan NetworkState, 10) ch := make(chan NetworkState, 10)
manager.subMutex.Lock() manager.subscribers.Store("test-client", ch)
manager.subscribers["test-client"] = ch
manager.subMutex.Unlock()
manager.notifySubscribers() manager.notifySubscribers()
@@ -63,19 +59,15 @@ func TestManager_NotifySubscribers_Debounce(t *testing.T) {
state: &NetworkState{ state: &NetworkState{
NetworkStatus: StatusWiFi, NetworkStatus: StatusWiFi,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan NetworkState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{}, dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
} }
manager.notifierWg.Add(1) manager.notifierWg.Add(1)
go manager.notifier() go manager.notifier()
ch := make(chan NetworkState, 10) ch := make(chan NetworkState, 10)
manager.subMutex.Lock() manager.subscribers.Store("test-client", ch)
manager.subscribers["test-client"] = ch
manager.subMutex.Unlock()
manager.notifySubscribers() manager.notifySubscribers()
manager.notifySubscribers() manager.notifySubscribers()
@@ -98,19 +90,15 @@ func TestManager_NotifySubscribers_Debounce(t *testing.T) {
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &NetworkState{}, state: &NetworkState{},
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan NetworkState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
} }
ch1 := make(chan NetworkState, 1) ch1 := make(chan NetworkState, 1)
ch2 := make(chan NetworkState, 1) ch2 := make(chan NetworkState, 1)
manager.subMutex.Lock() manager.subscribers.Store("client1", ch1)
manager.subscribers["client1"] = ch1 manager.subscribers.Store("client2", ch2)
manager.subscribers["client2"] = ch2
manager.subMutex.Unlock()
manager.Close() manager.Close()
@@ -125,31 +113,27 @@ func TestManager_Close(t *testing.T) {
assert.False(t, ok1, "ch1 should be closed") assert.False(t, ok1, "ch1 should be closed")
assert.False(t, ok2, "ch2 should be closed") assert.False(t, ok2, "ch2 should be closed")
assert.Len(t, manager.subscribers, 0) count := 0
manager.subscribers.Range(func(key string, ch chan NetworkState) bool { count++; return true })
assert.Equal(t, 0, count)
} }
func TestManager_Subscribe(t *testing.T) { func TestManager_Subscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &NetworkState{}, state: &NetworkState{},
subscribers: make(map[string]chan NetworkState),
subMutex: sync.RWMutex{},
} }
ch := manager.Subscribe("test-client") ch := manager.Subscribe("test-client")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Equal(t, 64, cap(ch)) assert.Equal(t, 64, cap(ch))
manager.subMutex.RLock() _, exists := manager.subscribers.Load("test-client")
_, exists := manager.subscribers["test-client"]
manager.subMutex.RUnlock()
assert.True(t, exists) assert.True(t, exists)
} }
func TestManager_Unsubscribe(t *testing.T) { func TestManager_Unsubscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &NetworkState{}, state: &NetworkState{},
subscribers: make(map[string]chan NetworkState),
subMutex: sync.RWMutex{},
} }
ch := manager.Subscribe("test-client") ch := manager.Subscribe("test-client")
@@ -159,9 +143,7 @@ func TestManager_Unsubscribe(t *testing.T) {
_, ok := <-ch _, ok := <-ch
assert.False(t, ok) assert.False(t, ok)
manager.subMutex.RLock() _, exists := manager.subscribers.Load("test-client")
_, exists := manager.subscribers["test-client"]
manager.subMutex.RUnlock()
assert.False(t, exists) assert.False(t, exists)
} }

View File

@@ -3,37 +3,29 @@ package network
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type SubscriptionBroker struct { type SubscriptionBroker struct {
mu sync.RWMutex pending syncmap.Map[string, chan PromptReply]
pending map[string]chan PromptReply requests syncmap.Map[string, PromptRequest]
requests map[string]PromptRequest pathSettingToToken syncmap.Map[string, string]
pathSettingToToken map[string]string
broadcastPrompt func(CredentialPrompt) broadcastPrompt func(CredentialPrompt)
} }
func NewSubscriptionBroker(broadcastPrompt func(CredentialPrompt)) PromptBroker { func NewSubscriptionBroker(broadcastPrompt func(CredentialPrompt)) PromptBroker {
return &SubscriptionBroker{ return &SubscriptionBroker{
pending: make(map[string]chan PromptReply), broadcastPrompt: broadcastPrompt,
requests: make(map[string]PromptRequest),
pathSettingToToken: make(map[string]string),
broadcastPrompt: broadcastPrompt,
} }
} }
func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string, error) { func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string, error) {
pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName) pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName)
b.mu.Lock() if existingToken, alreadyPending := b.pathSettingToToken.Load(pathSettingKey); alreadyPending {
existingToken, alreadyPending := b.pathSettingToToken[pathSettingKey]
b.mu.Unlock()
if alreadyPending {
log.Infof("[SubscriptionBroker] Duplicate prompt for %s, returning existing token", pathSettingKey) log.Infof("[SubscriptionBroker] Duplicate prompt for %s, returning existing token", pathSettingKey)
return existingToken, nil return existingToken, nil
} }
@@ -44,11 +36,9 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
replyChan := make(chan PromptReply, 1) replyChan := make(chan PromptReply, 1)
b.mu.Lock() b.pending.Store(token, replyChan)
b.pending[token] = replyChan b.requests.Store(token, req)
b.requests[token] = req b.pathSettingToToken.Store(pathSettingKey, token)
b.pathSettingToToken[pathSettingKey] = token
b.mu.Unlock()
if b.broadcastPrompt != nil { if b.broadcastPrompt != nil {
prompt := CredentialPrompt{ prompt := CredentialPrompt{
@@ -71,10 +61,7 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) { func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
return PromptReply{}, fmt.Errorf("unknown token: %s", token) return PromptReply{}, fmt.Errorf("unknown token: %s", token)
} }
@@ -93,10 +80,7 @@ func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptRepl
} }
func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error { func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
log.Warnf("[SubscriptionBroker] Resolve: unknown or expired token: %s", token) log.Warnf("[SubscriptionBroker] Resolve: unknown or expired token: %s", token)
return fmt.Errorf("unknown or expired token: %s", token) return fmt.Errorf("unknown or expired token: %s", token)
@@ -112,25 +96,19 @@ func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
} }
func (b *SubscriptionBroker) cleanup(token string) { func (b *SubscriptionBroker) cleanup(token string) {
b.mu.Lock() if req, exists := b.requests.Load(token); exists {
defer b.mu.Unlock()
if req, exists := b.requests[token]; exists {
pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName) pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName)
delete(b.pathSettingToToken, pathSettingKey) b.pathSettingToToken.Delete(pathSettingKey)
} }
delete(b.pending, token) b.pending.Delete(token)
delete(b.requests, token) b.requests.Delete(token)
} }
func (b *SubscriptionBroker) Cancel(path string, setting string) error { func (b *SubscriptionBroker) Cancel(path string, setting string) error {
pathSettingKey := fmt.Sprintf("%s:%s", path, setting) pathSettingKey := fmt.Sprintf("%s:%s", path, setting)
b.mu.Lock() token, exists := b.pathSettingToToken.Load(pathSettingKey)
token, exists := b.pathSettingToToken[pathSettingKey]
b.mu.Unlock()
if !exists { if !exists {
log.Infof("[SubscriptionBroker] Cancel: no pending prompt for %s", pathSettingKey) log.Infof("[SubscriptionBroker] Cancel: no pending prompt for %s", pathSettingKey)
return nil return nil

View File

@@ -6,10 +6,9 @@ func NewTestManager(backend Backend, state *NetworkState) *Manager {
state = &NetworkState{} state = &NetworkState{}
} }
return &Manager{ return &Manager{
backend: backend, backend: backend,
state: state, state: state,
subscribers: make(map[string]chan NetworkState), stopChan: make(chan struct{}),
stopChan: make(chan struct{}), dirty: make(chan struct{}, 1),
dirty: make(chan struct{}, 1),
} }
} }

View File

@@ -3,6 +3,7 @@ package network
import ( import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -108,14 +109,12 @@ type Manager struct {
backend Backend backend Backend
state *NetworkState state *NetworkState
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan NetworkState subscribers syncmap.Map[string, chan NetworkState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotifiedState *NetworkState lastNotifiedState *NetworkState
credentialSubscribers map[string]chan CredentialPrompt credentialSubscribers syncmap.Map[string, chan CredentialPrompt]
credSubMutex sync.RWMutex
} }
type EventType string type EventType string

View File

@@ -10,6 +10,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"time" "time"
@@ -27,6 +28,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
const APIVersion = 18 const APIVersion = 18
@@ -58,11 +60,9 @@ var wlrOutputManager *wlroutput.Manager
var evdevManager *evdev.Manager var evdevManager *evdev.Manager
var wlContext *wlcontext.SharedContext var wlContext *wlcontext.SharedContext
var capabilitySubscribers = make(map[string]chan ServerInfo) var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
var capabilityMutex sync.RWMutex var cupsSubscribers syncmap.Map[string, bool]
var cupsSubscriberCount atomic.Int32
var cupsSubscribers = make(map[string]bool)
var cupsSubscribersMutex sync.Mutex
func getSocketDir() string { func getSocketDir() string {
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" { if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
@@ -434,16 +434,14 @@ func getServerInfo() ServerInfo {
} }
func notifyCapabilityChange() { func notifyCapabilityChange() {
capabilityMutex.RLock()
defer capabilityMutex.RUnlock()
info := getServerInfo() info := getServerInfo()
for _, ch := range capabilitySubscribers { capabilitySubscribers.Range(func(key string, ch chan ServerInfo) bool {
select { select {
case ch <- info: case ch <- info:
default: default:
} }
} return true
})
} }
func handleSubscribe(conn net.Conn, req models.Request) { func handleSubscribe(conn net.Conn, req models.Request) {
@@ -475,18 +473,12 @@ func handleSubscribe(conn net.Conn, req models.Request) {
stopChan := make(chan struct{}) stopChan := make(chan struct{})
capChan := make(chan ServerInfo, 64) capChan := make(chan ServerInfo, 64)
capabilityMutex.Lock() capabilitySubscribers.Store(clientID+"-capabilities", capChan)
capabilitySubscribers[clientID+"-capabilities"] = capChan
capabilityMutex.Unlock()
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
defer func() { defer capabilitySubscribers.Delete(clientID + "-capabilities")
capabilityMutex.Lock()
delete(capabilitySubscribers, clientID+"-capabilities")
capabilityMutex.Unlock()
}()
for { for {
select { select {
@@ -728,12 +720,10 @@ func handleSubscribe(conn net.Conn, req models.Request) {
} }
if shouldSubscribe("cups") { if shouldSubscribe("cups") {
cupsSubscribersMutex.Lock() cupsSubscribers.Store(clientID+"-cups", true)
wasEmpty := len(cupsSubscribers) == 0 count := cupsSubscriberCount.Add(1)
cupsSubscribers[clientID+"-cups"] = true
cupsSubscribersMutex.Unlock()
if wasEmpty { if count == 1 {
if err := InitializeCupsManager(); err != nil { if err := InitializeCupsManager(); err != nil {
log.Warnf("Failed to initialize CUPS manager for subscription: %v", err) log.Warnf("Failed to initialize CUPS manager for subscription: %v", err)
} else { } else {
@@ -748,13 +738,10 @@ func handleSubscribe(conn net.Conn, req models.Request) {
defer wg.Done() defer wg.Done()
defer func() { defer func() {
cupsManager.Unsubscribe(clientID + "-cups") cupsManager.Unsubscribe(clientID + "-cups")
cupsSubscribers.Delete(clientID + "-cups")
count := cupsSubscriberCount.Add(-1)
cupsSubscribersMutex.Lock() if count == 0 {
delete(cupsSubscribers, clientID+"-cups")
isEmpty := len(cupsSubscribers) == 0
cupsSubscribersMutex.Unlock()
if isEmpty {
log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager") log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager")
if cupsManager != nil { if cupsManager != nil {
cupsManager.Close() cupsManager.Close()
@@ -822,36 +809,46 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}() }()
} }
if shouldSubscribe("extworkspace") && extWorkspaceManager != nil { if shouldSubscribe("extworkspace") {
wg.Add(1) if extWorkspaceManager == nil {
extWorkspaceChan := extWorkspaceManager.Subscribe(clientID + "-extworkspace") if err := InitializeExtWorkspaceManager(); err != nil {
go func() { log.Warnf("Failed to initialize ExtWorkspace manager for subscription: %v", err)
defer wg.Done() } else {
defer extWorkspaceManager.Unsubscribe(clientID + "-extworkspace") notifyCapabilityChange()
initialState := extWorkspaceManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "extworkspace", Data: initialState}:
case <-stopChan:
return
} }
}
for { 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 { select {
case state, ok := <-extWorkspaceChan: case eventChan <- ServiceEvent{Service: "extworkspace", Data: initialState}:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "extworkspace", Data: state}:
case <-stopChan:
return
}
case <-stopChan: case <-stopChan:
return 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 { if shouldSubscribe("brightness") && brightnessManager != nil {
@@ -1244,10 +1241,6 @@ func Start(printDocs bool) error {
log.Debugf("DWL manager unavailable: %v", err) log.Debugf("DWL manager unavailable: %v", err)
} }
if err := InitializeExtWorkspaceManager(); err != nil {
log.Debugf("ExtWorkspace manager unavailable: %v", err)
}
if err := InitializeWlrOutputManager(); err != nil { if err := InitializeWlrOutputManager(); err != nil {
log.Debugf("WlrOutput manager unavailable: %v", err) log.Debugf("WlrOutput manager unavailable: %v", err)
} }

View File

@@ -8,8 +8,8 @@ import (
"syscall" "syscall"
"time" "time"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
wlclient "github.com/yaslama/go-wayland/wayland/client"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
@@ -23,14 +23,13 @@ func NewManager(display *wlclient.Display, config Config) (*Manager, error) {
} }
m := &Manager{ m := &Manager{
config: config, config: config,
display: display, display: display,
ctx: display.Context(), ctx: display.Context(),
outputs: make(map[uint32]*outputState), cmdq: make(chan cmd, 128),
cmdq: make(chan cmd, 128), stopChan: make(chan struct{}),
stopChan: make(chan struct{}), updateTrigger: make(chan struct{}, 1),
updateTrigger: make(chan struct{}, 1),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
dbusSignal: make(chan *dbus.Signal, 16), dbusSignal: make(chan *dbus.Signal, 16),
transitionChan: make(chan int, 1), transitionChan: make(chan int, 1),
@@ -114,17 +113,17 @@ func (m *Manager) waylandActor() {
} }
func (m *Manager) allOutputsReady() bool { func (m *Manager) allOutputsReady() bool {
m.outputsMutex.RLock() hasOutputs := false
defer m.outputsMutex.RUnlock() allReady := true
if len(m.outputs) == 0 { m.outputs.Range(func(key uint32, value *outputState) bool {
return false hasOutputs = true
} if value.rampSize == 0 || value.failed {
for _, o := range m.outputs { allReady = false
if o.rampSize == 0 || o.failed {
return false return false
} }
} return true
return true })
return hasOutputs && allReady
} }
func (m *Manager) setupDBusMonitor() error { func (m *Manager) setupDBusMonitor() error {
@@ -157,7 +156,6 @@ func (m *Manager) setupRegistry() error {
m.registry = registry m.registry = registry
outputs := make([]*wlclient.Output, 0) outputs := make([]*wlclient.Output, 0)
outputRegNames := make(map[uint32]uint32)
outputNames := make(map[uint32]string) outputNames := make(map[uint32]string)
var gammaMgr *wlr_gamma_control.ZwlrGammaControlManagerV1 var gammaMgr *wlr_gamma_control.ZwlrGammaControlManagerV1
@@ -198,14 +196,9 @@ func (m *Manager) setupRegistry() error {
if gammaMgr != nil { if gammaMgr != nil {
outputs = append(outputs, output) outputs = append(outputs, output)
outputRegNames[outputID] = e.Name
} }
m.outputsMutex.Lock() m.outputRegNames.Store(outputID, e.Name)
if m.outputRegNames != nil {
m.outputRegNames[outputID] = e.Name
}
m.outputsMutex.Unlock()
m.configMutex.RLock() m.configMutex.RLock()
enabled := m.config.Enabled enabled := m.config.Enabled
@@ -236,23 +229,33 @@ func (m *Manager) setupRegistry() error {
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) { registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
m.post(func() { m.post(func() {
m.outputsMutex.Lock() var foundID uint32
defer m.outputsMutex.Unlock() var foundOut *outputState
m.outputs.Range(func(id uint32, out *outputState) bool {
for id, out := range m.outputs {
if out.registryName == e.Name { if out.registryName == e.Name {
log.Infof("Output %d (registry name %d) removed, destroying gamma control", id, e.Name) foundID = id
if out.gammaControl != nil { foundOut = out
control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) return false
control.Destroy() }
} return true
delete(m.outputs, id) })
if len(m.outputs) == 0 { if foundOut != nil {
m.controlsInitialized = false log.Infof("Output %d (registry name %d) removed, destroying gamma control", foundID, e.Name)
log.Info("All outputs removed, controls no longer initialized") if foundOut.gammaControl != nil {
} control := foundOut.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
return control.Destroy()
}
m.outputs.Delete(foundID)
hasOutputs := false
m.outputs.Range(func(key uint32, value *outputState) bool {
hasOutputs = true
return false
})
if !hasOutputs {
m.controlsInitialized = false
log.Info("All outputs removed, controls no longer initialized")
} }
} }
}) })
@@ -292,7 +295,6 @@ func (m *Manager) setupRegistry() error {
m.gammaControl = gammaMgr m.gammaControl = gammaMgr
m.availableOutputs = physicalOutputs m.availableOutputs = physicalOutputs
m.outputRegNames = outputRegNames
log.Info("setupRegistry: completed successfully (gamma controls will be initialized when enabled)") log.Info("setupRegistry: completed successfully (gamma controls will be initialized when enabled)")
return nil return nil
@@ -308,9 +310,12 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
continue continue
} }
outputID := output.ID()
registryName, _ := m.outputRegNames.Load(outputID)
outState := &outputState{ outState := &outputState{
id: output.ID(), id: outputID,
registryName: m.outputRegNames[output.ID()], registryName: registryName,
output: output, output: output,
gammaControl: control, gammaControl: control,
isVirtual: false, isVirtual: false,
@@ -318,14 +323,12 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
func(state *outputState) { func(state *outputState) {
control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) {
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(state.id); exists {
if outState, exists := m.outputs[state.id]; exists {
outState.rampSize = e.Size outState.rampSize = e.Size
outState.failed = false outState.failed = false
outState.retryCount = 0 outState.retryCount = 0
log.Infof("Output %d gamma_size=%d", state.id, e.Size) log.Infof("Output %d gamma_size=%d", state.id, e.Size)
} }
m.outputsMutex.Unlock()
m.transitionMutex.RLock() m.transitionMutex.RLock()
currentTemp := m.currentTemp currentTemp := m.currentTemp
@@ -337,8 +340,7 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
}) })
control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) {
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(state.id); exists {
if outState, exists := m.outputs[state.id]; exists {
outState.failed = true outState.failed = true
outState.rampSize = 0 outState.rampSize = 0
outState.retryCount++ outState.retryCount++
@@ -357,13 +359,10 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
}) })
}) })
} }
m.outputsMutex.Unlock()
}) })
}(outState) }(outState)
m.outputsMutex.Lock() m.outputs.Store(outputID, outState)
m.outputs[output.ID()] = outState
m.outputsMutex.Unlock()
} }
return nil return nil
@@ -375,8 +374,7 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
var outputName string var outputName string
output.SetNameHandler(func(ev wlclient.OutputNameEvent) { output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
outputName = ev.Name outputName = ev.Name
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(outputID); exists {
if outState, exists := m.outputs[outputID]; exists {
outState.name = ev.Name outState.name = ev.Name
if len(ev.Name) >= 9 && ev.Name[:9] == "HEADLESS-" { if len(ev.Name) >= 9 && ev.Name[:9] == "HEADLESS-" {
log.Infof("Detected virtual output %d (name=%s), marking for gamma control skip", outputID, ev.Name) log.Infof("Detected virtual output %d (name=%s), marking for gamma control skip", outputID, ev.Name)
@@ -384,7 +382,6 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
outState.failed = true outState.failed = true
} }
} }
m.outputsMutex.Unlock()
}) })
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
@@ -394,24 +391,24 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
return fmt.Errorf("failed to get gamma control: %w", err) return fmt.Errorf("failed to get gamma control: %w", err)
} }
registryName, _ := m.outputRegNames.Load(outputID)
outState := &outputState{ outState := &outputState{
id: outputID, id: outputID,
name: outputName, name: outputName,
registryName: m.outputRegNames[outputID], registryName: registryName,
output: output, output: output,
gammaControl: control, gammaControl: control,
isVirtual: false, isVirtual: false,
} }
control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) {
m.outputsMutex.Lock() if out, exists := m.outputs.Load(outState.id); exists {
if out, exists := m.outputs[outState.id]; exists {
out.rampSize = e.Size out.rampSize = e.Size
out.failed = false out.failed = false
out.retryCount = 0 out.retryCount = 0
log.Infof("Output %d gamma_size=%d", outState.id, e.Size) log.Infof("Output %d gamma_size=%d", outState.id, e.Size)
} }
m.outputsMutex.Unlock()
m.transitionMutex.RLock() m.transitionMutex.RLock()
currentTemp := m.currentTemp currentTemp := m.currentTemp
@@ -423,8 +420,7 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
}) })
control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) {
m.outputsMutex.Lock() if out, exists := m.outputs.Load(outState.id); exists {
if out, exists := m.outputs[outState.id]; exists {
out.failed = true out.failed = true
out.rampSize = 0 out.rampSize = 0
out.retryCount++ out.retryCount++
@@ -443,12 +439,9 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
}) })
}) })
} }
m.outputsMutex.Unlock()
}) })
m.outputsMutex.Lock() m.outputs.Store(outputID, outState)
m.outputs[output.ID()] = outState
m.outputsMutex.Unlock()
log.Infof("Added gamma control for output %d", output.ID()) log.Infof("Added gamma control for output %d", output.ID())
return nil return nil
@@ -623,17 +616,19 @@ func (m *Manager) transitionWorker() {
if !enabled && targetTemp == identityTemp && m.controlsInitialized { if !enabled && targetTemp == identityTemp && m.controlsInitialized {
m.post(func() { m.post(func() {
log.Info("Destroying gamma controls after transition to identity") log.Info("Destroying gamma controls after transition to identity")
m.outputsMutex.Lock() m.outputs.Range(func(id uint32, out *outputState) bool {
for id, out := range m.outputs {
if out.gammaControl != nil { if out.gammaControl != nil {
control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
control.Destroy() control.Destroy()
log.Debugf("Destroyed gamma control for output %d", id) log.Debugf("Destroyed gamma control for output %d", id)
} }
} return true
m.outputs = make(map[uint32]*outputState) })
m.outputs.Range(func(key uint32, value *outputState) bool {
m.outputs.Delete(key)
return true
})
m.controlsInitialized = false m.controlsInitialized = false
m.outputsMutex.Unlock()
m.transitionMutex.Lock() m.transitionMutex.Lock()
m.currentTemp = identityTemp m.currentTemp = identityTemp
@@ -661,9 +656,7 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
return nil return nil
} }
m.outputsMutex.RLock() _, exists := m.outputs.Load(out.id)
_, exists := m.outputs[out.id]
m.outputsMutex.RUnlock()
if !exists { if !exists {
return nil return nil
@@ -689,14 +682,12 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
state := out state := out
control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) {
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(state.id); exists {
if outState, exists := m.outputs[state.id]; exists {
outState.rampSize = e.Size outState.rampSize = e.Size
outState.failed = false outState.failed = false
outState.retryCount = 0 outState.retryCount = 0
log.Infof("Output %d gamma_size=%d (recreated)", state.id, e.Size) log.Infof("Output %d gamma_size=%d (recreated)", state.id, e.Size)
} }
m.outputsMutex.Unlock()
m.transitionMutex.RLock() m.transitionMutex.RLock()
currentTemp := m.currentTemp currentTemp := m.currentTemp
@@ -708,8 +699,7 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
}) })
control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) {
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(state.id); exists {
if outState, exists := m.outputs[state.id]; exists {
outState.failed = true outState.failed = true
outState.rampSize = 0 outState.rampSize = 0
outState.retryCount++ outState.retryCount++
@@ -728,7 +718,6 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
}) })
}) })
} }
m.outputsMutex.Unlock()
}) })
out.gammaControl = control out.gammaControl = control
@@ -750,13 +739,11 @@ func (m *Manager) applyNowOnActor(temp int) {
return return
} }
// Lock while snapshotting outputs to prevent races with recreateOutputControl
m.outputsMutex.RLock()
var outs []*outputState var outs []*outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, value *outputState) bool {
outs = append(outs, out) outs = append(outs, value)
} return true
m.outputsMutex.RUnlock() })
if len(outs) == 0 { if len(outs) == 0 {
return return
@@ -796,20 +783,17 @@ func (m *Manager) applyNowOnActor(temp int) {
if err := m.setGammaBytesActor(j.out, j.data); err != nil { if err := m.setGammaBytesActor(j.out, j.data); err != nil {
log.Warnf("Failed to set gamma for output %d: %v", j.out.id, err) log.Warnf("Failed to set gamma for output %d: %v", j.out.id, err)
outID := j.out.id outID := j.out.id
m.outputsMutex.Lock() if out, exists := m.outputs.Load(outID); exists {
if out, exists := m.outputs[outID]; exists {
out.failed = true out.failed = true
out.rampSize = 0 out.rampSize = 0
} }
m.outputsMutex.Unlock()
time.AfterFunc(300*time.Millisecond, func() { time.AfterFunc(300*time.Millisecond, func() {
m.post(func() { m.post(func() {
m.outputsMutex.RLock() if out, exists := m.outputs.Load(outID); exists {
out, exists := m.outputs[outID] if out.failed {
m.outputsMutex.RUnlock() m.recreateOutputControl(out)
if exists && out.failed { }
m.recreateOutputControl(out)
} }
}) })
}) })
@@ -935,28 +919,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
if m.lastNotified != nil && !stateChanged(m.lastNotified, &currentState) { if m.lastNotified != nil && !stateChanged(m.lastNotified, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan State) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -1296,17 +1273,19 @@ func (m *Manager) SetEnabled(enabled bool) {
if currentTemp == identityTemp { if currentTemp == identityTemp {
m.post(func() { m.post(func() {
log.Infof("Already at %dK, destroying gamma controls immediately", identityTemp) log.Infof("Already at %dK, destroying gamma controls immediately", identityTemp)
m.outputsMutex.Lock() m.outputs.Range(func(id uint32, out *outputState) bool {
for id, out := range m.outputs {
if out.gammaControl != nil { if out.gammaControl != nil {
control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
control.Destroy() control.Destroy()
log.Debugf("Destroyed gamma control for output %d", id) log.Debugf("Destroyed gamma control for output %d", id)
} }
} return true
m.outputs = make(map[uint32]*outputState) })
m.outputs.Range(func(key uint32, value *outputState) bool {
m.outputs.Delete(key)
return true
})
m.controlsInitialized = false m.controlsInitialized = false
m.outputsMutex.Unlock()
m.transitionMutex.Lock() m.transitionMutex.Lock()
m.currentTemp = identityTemp m.currentTemp = identityTemp
@@ -1332,21 +1311,22 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.outputsMutex.Lock() m.outputs.Range(func(key uint32, out *outputState) bool {
for _, out := range m.outputs {
if control, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok { if control, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok {
control.Destroy() control.Destroy()
} }
} return true
m.outputs = make(map[uint32]*outputState) })
m.outputsMutex.Unlock() m.outputs.Range(func(key uint32, value *outputState) bool {
m.outputs.Delete(key)
return true
})
if manager, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1); ok { if manager, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1); ok {
manager.Destroy() manager.Destroy()

View File

@@ -6,8 +6,9 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
wlclient "github.com/yaslama/go-wayland/wayland/client"
) )
type Config struct { type Config struct {
@@ -48,9 +49,8 @@ type Manager struct {
registry *wlclient.Registry registry *wlclient.Registry
gammaControl interface{} gammaControl interface{}
availableOutputs []*wlclient.Output availableOutputs []*wlclient.Output
outputRegNames map[uint32]uint32 outputRegNames syncmap.Map[uint32, uint32]
outputs map[uint32]*outputState outputs syncmap.Map[uint32, *outputState]
outputsMutex sync.RWMutex
controlsInitialized bool controlsInitialized bool
cmdq chan cmd cmdq chan cmd
@@ -69,8 +69,7 @@ type Manager struct {
cachedIPLon *float64 cachedIPLon *float64
locationMutex sync.RWMutex locationMutex sync.RWMutex
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -147,19 +146,14 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {

View File

@@ -6,7 +6,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
type SharedContext struct { type SharedContext struct {

View File

@@ -154,14 +154,13 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
statusChan <- fmt.Errorf("configuration cancelled (outdated serial)") statusChan <- fmt.Errorf("configuration cancelled (outdated serial)")
}) })
m.headsMutex.RLock()
headsByName := make(map[string]*headState) headsByName := make(map[string]*headState)
for _, head := range m.heads { m.heads.Range(func(key uint32, head *headState) bool {
if !head.finished { if !head.finished {
headsByName[head.name] = head headsByName[head.name] = head
} }
} return true
m.headsMutex.RUnlock() })
for _, headCfg := range heads { for _, headCfg := range heads {
head, exists := headsByName[headCfg.Name] head, exists := headsByName[headCfg.Name]
@@ -188,9 +187,7 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
} }
if headCfg.ModeID != nil { if headCfg.ModeID != nil {
m.modesMutex.RLock() mode, exists := m.modes.Load(*headCfg.ModeID)
mode, exists := m.modes[*headCfg.ModeID]
m.modesMutex.RUnlock()
if !exists { if !exists {
config.Destroy() config.Destroy()

View File

@@ -6,20 +6,17 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
func NewManager(display *wlclient.Display) (*Manager, error) { func NewManager(display *wlclient.Display) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
ctx: display.Context(), ctx: display.Context(),
heads: make(map[uint32]*headState), cmdq: make(chan cmd, 128),
modes: make(map[uint32]*modeState), stopChan: make(chan struct{}),
cmdq: make(chan cmd, 128), dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}), fatalError: make(chan error, 1),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1),
fatalError: make(chan error, 1),
} }
m.wg.Add(1) m.wg.Add(1)
@@ -143,9 +140,7 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
modeIDs: make([]uint32, 0), modeIDs: make([]uint32, 0),
} }
m.headsMutex.Lock() m.heads.Store(headID, head)
m.heads[headID] = head
m.headsMutex.Unlock()
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) { handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name) log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name)
@@ -254,9 +249,7 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
log.Debugf("WlrOutput: Head %d finished", headID) log.Debugf("WlrOutput: Head %d finished", headID)
head.finished = true head.finished = true
m.headsMutex.Lock() m.heads.Delete(headID)
delete(m.heads, headID)
m.headsMutex.Unlock()
m.post(func() { m.post(func() {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -279,15 +272,12 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
handle: handle, handle: handle,
} }
m.modesMutex.Lock() m.modes.Store(modeID, mode)
m.modes[modeID] = mode
m.modesMutex.Unlock()
m.headsMutex.Lock() if head, ok := m.heads.Load(headID); ok {
if head, ok := m.heads[headID]; ok {
head.modeIDs = append(head.modeIDs, modeID) head.modeIDs = append(head.modeIDs, modeID)
m.heads.Store(headID, head)
} }
m.headsMutex.Unlock()
handle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) { handle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
log.Debugf("WlrOutput: Mode %d size: %dx%d", modeID, e.Width, e.Height) log.Debugf("WlrOutput: Mode %d size: %dx%d", modeID, e.Width, e.Height)
@@ -318,9 +308,7 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
log.Debugf("WlrOutput: Mode %d finished", modeID) log.Debugf("WlrOutput: Mode %d finished", modeID)
mode.finished = true mode.finished = true
m.modesMutex.Lock() m.modes.Delete(modeID)
delete(m.modes, modeID)
m.modesMutex.Unlock()
m.post(func() { m.post(func() {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -333,22 +321,22 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
} }
func (m *Manager) updateState() { func (m *Manager) updateState() {
m.headsMutex.RLock()
m.modesMutex.RLock()
outputs := make([]Output, 0) outputs := make([]Output, 0)
for _, head := range m.heads { m.heads.Range(func(key uint32, head *headState) bool {
if head.finished { if head.finished {
continue return true
} }
modes := make([]OutputMode, 0) modes := make([]OutputMode, 0)
var currentMode *OutputMode var currentMode *OutputMode
for _, modeID := range head.modeIDs { for _, modeID := range head.modeIDs {
mode, exists := m.modes[modeID] mode, exists := m.modes.Load(modeID)
if !exists || mode.finished { if !exists {
continue
}
if mode.finished {
continue continue
} }
@@ -385,10 +373,8 @@ func (m *Manager) updateState() {
ID: head.id, ID: head.id,
} }
outputs = append(outputs, output) outputs = append(outputs, output)
} return true
})
m.modesMutex.RUnlock()
m.headsMutex.RUnlock()
newState := State{ newState := State{
Outputs: outputs, Outputs: outputs,
@@ -442,14 +428,6 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
subCount := len(m.subscribers)
m.subMutex.RUnlock()
if subCount == 0 {
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
@@ -458,15 +436,14 @@ func (m *Manager) notifier() {
continue continue
} }
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
log.Warn("WlrOutput: subscriber channel full, dropping update") log.Warn("WlrOutput: subscriber channel full, dropping update")
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -480,30 +457,27 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.modesMutex.Lock() m.modes.Range(func(key uint32, mode *modeState) bool {
for _, mode := range m.modes {
if mode.handle != nil { if mode.handle != nil {
mode.handle.Release() mode.handle.Release()
} }
} m.modes.Delete(key)
m.modes = make(map[uint32]*modeState) return true
m.modesMutex.Unlock() })
m.headsMutex.Lock() m.heads.Range(func(key uint32, head *headState) bool {
for _, head := range m.heads {
if head.handle != nil { if head.handle != nil {
head.handle.Release() head.handle.Release()
} }
} m.heads.Delete(key)
m.heads = make(map[uint32]*headState) return true
m.headsMutex.Unlock() })
if m.manager != nil { if m.manager != nil {
m.manager.Stop() m.manager.Stop()

View File

@@ -4,7 +4,8 @@ import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type OutputMode struct { type OutputMode struct {
@@ -49,11 +50,8 @@ type Manager struct {
registry *wlclient.Registry registry *wlclient.Registry
manager *wlr_output_management.ZwlrOutputManagerV1 manager *wlr_output_management.ZwlrOutputManagerV1
headsMutex sync.RWMutex heads syncmap.Map[uint32, *headState]
heads map[uint32]*headState modes syncmap.Map[uint32, *modeState]
modesMutex sync.RWMutex
modes map[uint32]*modeState
serial uint32 serial uint32
@@ -62,8 +60,7 @@ type Manager struct {
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -120,19 +117,19 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok { if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch) close(val)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {

View File

@@ -0,0 +1,3 @@
// Keep this sorted
rajveermalviya

View File

@@ -0,0 +1,24 @@
Copyright 2021 go-wayland authors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,25 @@
# Wayland implementation in Go
[![Go Reference](https://pkg.go.dev/badge/github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland.svg)](https://pkg.go.dev/github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland)
This module contains pure Go implementation of the Wayland protocol.
Currently only wayland-client functionality is supported.
Go code is generated from protocol XML files using
[`go-wayland-scanner`](cmd/go-wayland-scanner/scanner.go).
To load cursor, minimal port of `wayland-cursor` & `xcursor` in pure Go
is located at [`wayland/cursor`](wayland/cursor) & [`wayland/cursor/xcursor`](wayland/cursor/xcursor)
respectively.
To demonstrate the functionality of this module
[`examples/imageviewer`](examples/imageviewer) contains a simple image
viewer. It demos displaying a top-level window, resizing of window,
cursor themes, pointer and keyboard. Because it's in pure Go, it can be
compiled without CGO. You can try it using the following commands:
```sh
CGO_ENABLED=0 go install github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/examples/imageviewer@latest
imageviewer file.jpg
```

4
core/pkg/go-wayland/generate Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
cd ./wayland
go generate -x ./...

9
core/pkg/go-wayland/generatep Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
# Runs go generate for each directory, but in parallel. Any arguments are appended to the
# go generate command.
# Usage: $ ./generatep [go generate arguments]
# Print all generate commands: $ ./generatep -x
cd ./wayland
find . -type f -name '*.go' -exec dirname {} \; | sort -u | parallel -j 0 go generate $1 {}/.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
package client
type Dispatcher interface {
Dispatch(opcode uint32, fd int, data []byte)
}
type Proxy interface {
Context() *Context
SetContext(ctx *Context)
ID() uint32
SetID(id uint32)
}
type BaseProxy struct {
ctx *Context
id uint32
}
func (p *BaseProxy) ID() uint32 {
return p.id
}
func (p *BaseProxy) SetID(id uint32) {
p.id = id
}
func (p *BaseProxy) Context() *Context {
return p.ctx
}
func (p *BaseProxy) SetContext(ctx *Context) {
p.ctx = ctx
}

View File

@@ -0,0 +1,112 @@
package client
import (
"errors"
"fmt"
"net"
"os"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type Context struct {
conn *net.UnixConn
objects syncmap.Map[uint32, Proxy] // map[uint32]Proxy - thread-safe concurrent map
currentID uint32
idMu sync.Mutex // protects currentID increment
}
func (ctx *Context) Register(p Proxy) {
ctx.idMu.Lock()
ctx.currentID++
id := ctx.currentID
ctx.idMu.Unlock()
p.SetID(id)
p.SetContext(ctx)
ctx.objects.Store(id, p)
}
func (ctx *Context) Unregister(p Proxy) {
ctx.objects.Delete(p.ID())
}
func (ctx *Context) GetProxy(id uint32) Proxy {
if val, ok := ctx.objects.Load(id); ok {
return val
}
return nil
}
func (ctx *Context) Close() error {
return ctx.conn.Close()
}
// Dispatch reads and processes incoming messages and calls [client.Dispatcher.Dispatch] on the
// respective wayland protocol.
// Dispatch must be called on the same goroutine as other interactions with the Context.
// If a multi goroutine approach is desired, use [Context.GetDispatch] instead.
// Dispatch blocks if there are no incoming messages.
// A Dispatch loop is usually used to handle incoming messages.
func (ctx *Context) Dispatch() error {
return ctx.GetDispatch()()
}
var ErrDispatchSenderNotFound = errors.New("dispatch: unable to find sender")
var ErrDispatchSenderUnsupported = errors.New("dispatch: sender does not implement Dispatch method")
var ErrDispatchUnableToReadMsg = errors.New("dispatch: unable to read msg")
// GetDispatch reads incoming messages and returns the dispatch function which calls
// [client.Dispatcher.Dispatch] on the respective wayland protocol.
// This function is now thread-safe and can be called from multiple goroutines.
// GetDispatch blocks if there are no incoming messages.
func (ctx *Context) GetDispatch() func() error {
senderID, opcode, fd, data, err := ctx.ReadMsg() // Blocks if there are no incoming messages
if err != nil {
return func() error {
return fmt.Errorf("%w: %w", ErrDispatchUnableToReadMsg, err)
}
}
return func() error {
proxy, ok := ctx.objects.Load(senderID)
if !ok {
return fmt.Errorf("%w (senderID=%d)", ErrDispatchSenderNotFound, senderID)
}
sender, ok := proxy.(Dispatcher)
if !ok {
return fmt.Errorf("%w (senderID=%d)", ErrDispatchSenderUnsupported, senderID)
}
sender.Dispatch(opcode, fd, data)
return nil
}
}
func Connect(addr string) (*Display, error) {
if addr == "" {
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDir == "" {
return nil, errors.New("env XDG_RUNTIME_DIR not set")
}
if addr == "" {
addr = os.Getenv("WAYLAND_DISPLAY")
}
if addr == "" {
addr = "wayland-0"
}
addr = runtimeDir + "/" + addr
}
ctx := &Context{}
conn, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: addr, Net: "unix"})
if err != nil {
return nil, err
}
ctx.conn = conn
return NewDisplay(ctx), nil
}

View File

@@ -0,0 +1,111 @@
package client_test
import (
"errors"
"fmt"
"log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
// Shows a dispatch loop that will block the goroutine.
// This approach has no risk of data races but the loop blocks the goroutine when no messages are
// received. This can be a valid approach if there are no more changes that need to be made after
// setting up and starting the loop.
// For a multi goroutine approach, use [client.Context.GetDispatch].
func ExampleContext_Dispatch() {
display, err := client.Connect("")
if err != nil {
log.Fatalf("Error connecting to Wayland server: %v", err)
}
registry, err := display.GetRegistry()
if err != nil {
log.Fatalf("Error getting Wayland registry: %v", err)
}
var seat *client.Seat
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case client.SeatInterfaceName:
seat = client.NewSeat(display.Context())
err := registry.Bind(e.Name, e.Interface, e.Version, seat)
if err != nil {
log.Fatalf("unable to bind %s interface: %v", client.SeatInterfaceName, err)
}
}
})
display.Roundtrip()
display.Roundtrip()
keyboard, err := seat.GetKeyboard()
if err != nil {
log.Printf("Error getting keyboard: %v", err)
}
log.Printf("Got keyboard: %v\n", keyboard)
for {
err := display.Context().Dispatch()
if err != nil {
log.Printf("Dispatch error: %v\n", err)
}
}
}
// Shows how the dispatch loop can be done in another goroutine.
// This prevents the goroutine from being blocked and allows making changes to wayland objects while
// the dispatch loop is blocking another goroutine.
func ExampleContext_GetDispatch() {
display, err := client.Connect("")
if err != nil {
log.Fatalf("Error connecting to Wayland server: %v", err)
}
registry, err := display.GetRegistry()
if err != nil {
log.Fatalf("Error getting Wayland registry: %v", err)
}
var seat *client.Seat
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case client.SeatInterfaceName:
seat = client.NewSeat(display.Context())
err := registry.Bind(e.Name, e.Interface, e.Version, seat)
if err != nil {
log.Fatalf("unable to bind %s interface: %v", client.SeatInterfaceName, err)
}
}
})
display.Roundtrip()
display.Roundtrip()
dispatchQueue := make(chan func() error)
go func() {
for {
dispatchQueue <- display.Context().GetDispatch()
}
}()
keyboard, err := seat.GetKeyboard()
if err != nil {
log.Printf("Error getting keyboard: %v", err)
}
log.Printf("Got keyboard: %v\n", keyboard)
err = errors.Join(keyboard.Release(), seat.Release(), display.Context().Close())
if err != nil {
fmt.Printf("Error cleaning up: %v\n", err)
}
for {
select {
// Add other cases here to do other things
case dispatchFunc := <-dispatchQueue:
err := dispatchFunc()
if err != nil {
log.Printf("Dispatch error: %v\n", err)
}
}
}
}

View File

@@ -0,0 +1,37 @@
package client
import (
"fmt"
"log"
)
// Roundtrip blocks until all pending request are processed by the server.
// It is the implementation of [wl_display_roundtrip].
//
// [wl_display_roundtrip]: https://wayland.freedesktop.org/docs/html/apb.html#Client-classwl__display_1ab60f38c2f80980ac84f347e932793390
func (i *Display) Roundtrip() error {
callback, err := i.Sync()
if err != nil {
return fmt.Errorf("unable to get sync callback: %w", err)
}
defer func() {
if err2 := callback.Destroy(); err2 != nil {
log.Printf("unable to destroy callback: %v\n", err2)
}
}()
done := false
callback.SetDoneHandler(func(_ CallbackDoneEvent) {
done = true
})
// Wait for callback to return
for !done {
err := i.Context().GetDispatch()()
if err != nil {
return fmt.Errorf("roundtrip: failed to dispatch: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,6 @@
// Package client is Go port of wayland-client library
// for writing pure Go GUI software for wayland supported
// platforms.
package client
//go:generate go run github.com/yaslama/go-wayland/cmd/go-wayland-scanner -pkg client -prefix wl -o client.go -i https://gitlab.freedesktop.org/wayland/wayland/-/raw/1.23.0/protocol/wayland.xml?ref_type=tags

View File

@@ -0,0 +1,120 @@
package client
import (
"bytes"
"fmt"
"unsafe"
"golang.org/x/sys/unix"
_ "unsafe"
)
var oobSpace = unix.CmsgSpace(4)
func (ctx *Context) ReadMsg() (senderID uint32, opcode uint32, fd int, msg []byte, err error) {
fd = -1
oob := make([]byte, oobSpace)
header := make([]byte, 8)
n, oobn, _, _, err := ctx.conn.ReadMsgUnix(header, oob)
if err != nil {
return senderID, opcode, fd, msg, err
}
if n != 8 {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: incorrect number of bytes read for header (n=%d)", n)
}
if oobn > 0 {
fds, err := getFdsFromOob(oob, oobn, "header")
if err != nil {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: %w", err)
}
if len(fds) > 0 {
fd = fds[0]
}
}
senderID = Uint32(header[:4])
opcodeAndSize := Uint32(header[4:8])
opcode = opcodeAndSize & 0xffff
size := opcodeAndSize >> 16
msgSize := int(size) - 8
if msgSize == 0 {
return senderID, opcode, fd, nil, nil
}
msg = make([]byte, msgSize)
if fd == -1 {
// if something was read before, then zero it out
if oobn > 0 {
oob = make([]byte, oobSpace)
}
n, oobn, _, _, err = ctx.conn.ReadMsgUnix(msg, oob)
} else {
n, err = ctx.conn.Read(msg)
}
if err != nil {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: %w", err)
}
if n != msgSize {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: incorrect number of bytes read for msg (n=%d, msgSize=%d)", n, msgSize)
}
if fd == -1 && oobn > 0 {
fds, err := getFdsFromOob(oob, oobn, "msg")
if err != nil {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: %w", err)
}
if len(fds) > 0 {
fd = fds[0]
}
}
return senderID, opcode, fd, msg, nil
}
func getFdsFromOob(oob []byte, oobn int, source string) ([]int, error) {
if oobn > len(oob) {
return nil, fmt.Errorf("getFdsFromOob: incorrect number of bytes read from %s for oob (oobn=%d)", source, oobn)
}
scms, err := unix.ParseSocketControlMessage(oob)
if err != nil {
return nil, fmt.Errorf("getFdsFromOob: unable to parse control message from %s: %w", source, err)
}
var fdsRet []int
for _, scm := range scms {
fds, err := unix.ParseUnixRights(&scm)
if err != nil {
return nil, fmt.Errorf("getFdsFromOob: unable to parse unix rights from %s: %w", source, err)
}
fdsRet = append(fdsRet, fds...)
}
return fdsRet, nil
}
func Uint32(src []byte) uint32 {
_ = src[3]
return *(*uint32)(unsafe.Pointer(&src[0]))
}
func String(src []byte) string {
idx := bytes.IndexByte(src, 0)
src = src[:idx:idx]
return *(*string)(unsafe.Pointer(&src))
}
func Fixed(src []byte) float64 {
_ = src[3]
fx := *(*int32)(unsafe.Pointer(&src[0]))
return fixedToFloat64(fx)
}

View File

@@ -0,0 +1,44 @@
package client
import (
"fmt"
"unsafe"
)
func (ctx *Context) WriteMsg(b []byte, oob []byte) error {
n, oobn, err := ctx.conn.WriteMsgUnix(b, oob, nil)
if err != nil {
return err
}
if n != len(b) || oobn != len(oob) {
return fmt.Errorf("ctx.WriteMsg: incorrect number of bytes written (n=%d oobn=%d)", n, oobn)
}
return nil
}
func PutUint32(dst []byte, v uint32) {
_ = dst[3]
*(*uint32)(unsafe.Pointer(&dst[0])) = v
}
func PutFixed(dst []byte, f float64) {
fx := fixedFromfloat64(f)
_ = dst[3]
*(*int32)(unsafe.Pointer(&dst[0])) = fx
}
// PutString places a string in Wayland's wire format on the destination buffer.
// It first places the length of the string (plus one for the null terminator) and then the string
// followed by a null byte.
// The length of dst must be equal to, or greater than, len(v) + 5.
func PutString(dst []byte, v string) {
PutUint32(dst[:4], uint32(len(v)+1))
copy(dst[4:], v)
dst[4+len(v)] = '\x00' // To cause panic if dst is not large enough
}
func PutArray(dst []byte, a []byte) {
PutUint32(dst[:4], uint32(len(a)))
copy(dst[4:], a)
}

View File

@@ -0,0 +1,24 @@
package client
import "math"
// From wayland/wayland-util.h
func fixedToFloat64(f int32) float64 {
u_i := (1023+44)<<52 + (1 << 51) + int64(f)
u_d := math.Float64frombits(uint64(u_i))
return u_d - (3 << 43)
}
func fixedFromfloat64(d float64) int32 {
u_d := d + (3 << (51 - 8))
u_i := int64(math.Float64bits(u_d))
return int32(u_i)
}
func PaddedLen(l int) int {
if (l & 0x3) != 0 {
return l + (4 - (l & 0x3))
}
return l
}

28
core/pkg/syncmap/LICENSE Normal file
View File

@@ -0,0 +1,28 @@
Copyright 2009 The Go Authors.
Copyright 2024 Zachary Olstein.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

537
core/pkg/syncmap/syncmap.go Normal file
View File

@@ -0,0 +1,537 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package syncmap
import (
"sync"
"sync/atomic"
"unsafe"
)
// Map is like a Go map[K]V but is safe for concurrent use
// by multiple goroutines without additional locking or coordination.
// Loads, stores, and deletes run in amortized constant time.
//
// The Map type is specialized. Most code should use a plain Go map instead,
// with separate locking or coordination, for better type safety and to make it
// easier to maintain other invariants along with the map content.
//
// The Map type is optimized for two common use cases: (1) when the entry for a given
// key is only ever written once but read many times, as in caches that only grow,
// or (2) when multiple goroutines read, write, and overwrite entries for disjoint
// sets of keys. In these two cases, use of a Map may significantly reduce lock
// contention compared to a Go map paired with a separate [Mutex] or [RWMutex].
//
// The zero Map is empty and ready for use. A Map must not be copied after first use.
//
// In the terminology of [the Go memory model], Map arranges that a write operation
// “synchronizes before” any read operation that observes the effect of the write, where
// read and write operations are defined as follows.
// [Map.Load], [Map.LoadAndDelete], [Map.LoadOrStore], and [Map.Swap] are read operations;
// [Map.Delete], [Map.LoadAndDelete], [Map.Store], and [Map.Swap] are write operations;
// [Map.LoadOrStore] is a write operation when it returns loaded set to false.
//
// [the Go memory model]: https://go.dev/ref/mem
type Map[K comparable, V any] struct {
mu sync.Mutex
// read contains the portion of the map's contents that are safe for
// concurrent access (with or without mu held).
//
// The read field itself is always safe to load, but must only be stored with
// mu held.
//
// Entries stored in read may be updated concurrently without mu, but updating
// a previously-expunged entry requires that the entry be copied to the dirty
// map and unexpunged with mu held.
read atomic.Pointer[readOnly[K, V]]
// dirty contains the portion of the map's contents that require mu to be
// held. To ensure that the dirty map can be promoted to the read map quickly,
// it also includes all of the non-expunged entries in the read map.
//
// Expunged entries are not stored in the dirty map. An expunged entry in the
// clean map must be unexpunged and added to the dirty map before a new value
// can be stored to it.
//
// If the dirty map is nil, the next write to the map will initialize it by
// making a shallow copy of the clean map, omitting stale entries.
dirty map[K]*entry[V]
// misses counts the number of loads since the read map was last updated that
// needed to lock mu to determine whether the key was present.
//
// Once enough misses have occurred to cover the cost of copying the dirty
// map, the dirty map will be promoted to the read map (in the unamended
// state) and the next store to the map will make a new dirty copy.
misses int
}
// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly[K comparable, V any] struct {
m map[K]*entry[V]
amended bool // true if the dirty map contains some key not in m.
}
// expunged is an arbitrary pointer that marks entries which have been deleted
// from the dirty map.
// Because the same expunged pointer is used regardless of the Map's value type,
// value pointers read from the map must be compared against expunged BEFORE
// casting the pointer to *V.
var expunged = unsafe.Pointer(new(int))
// An entry is a slot in the map corresponding to a particular key.
type entry[V any] struct {
// p points to the value stored for the entry.
//
// If p == nil, the entry has been deleted, and either m.dirty == nil or
// m.dirty[key] is e.
//
// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
// is missing from m.dirty.
//
// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
// != nil, in m.dirty[key].
//
// If p != expunged, it is always safe to cast it to (*V).
//
// An entry can be deleted by atomic replacement with nil: when m.dirty is
// next created, it will atomically replace nil with expunged and leave
// m.dirty[key] unset.
//
// An entry's associated value can be updated by atomic replacement, provided
// p != expunged. If p == expunged, an entry's associated value can be updated
// only after first setting m.dirty[key] = e so that lookups using the dirty
// map find the entry.
p unsafe.Pointer
}
func newEntry[V any](i V) *entry[V] {
e := &entry[V]{}
atomic.StorePointer(&e.p, unsafe.Pointer(&i))
return e
}
func (m *Map[K, V]) loadReadOnly() readOnly[K, V] {
if p := m.read.Load(); p != nil {
return *p
}
return readOnly[K, V]{}
}
// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map[K, V]) Load(key K) (value V, ok bool) {
read := m.loadReadOnly()
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
// Avoid reporting a spurious miss if m.dirty got promoted while we were
// blocked on m.mu. (If further loads of the same key will not miss, it's
// not worth copying the dirty map for this key.)
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return value, false
}
return e.load()
}
func (e *entry[V]) load() (value V, ok bool) {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return value, false
}
return *(*V)(p), true
}
// Store sets the value for a key.
func (m *Map[K, V]) Store(key K, value V) {
_, _ = m.Swap(key, value)
}
// unexpungeLocked ensures that the entry is not marked as expunged.
//
// If the entry was previously expunged, it must be added to the dirty map
// before m.mu is unlocked.
func (e *entry[V]) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
// swapLocked unconditionally swaps a value into the entry.
//
// The entry must be known not to be expunged.
func (e *entry[V]) swapLocked(i *V) *V {
return (*V)(atomic.SwapPointer(&e.p, unsafe.Pointer(i)))
}
// LoadOrStore returns the existing value for the key if present.
// Otherwise, it stores and returns the given value.
// The loaded result is true if the value was loaded, false if stored.
func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
// Avoid locking if it's a clean hit.
read := m.loadReadOnly()
if e, ok := read.m[key]; ok {
actual, loaded, ok := e.tryLoadOrStore(value)
if ok {
return actual, loaded
}
}
m.mu.Lock()
read = m.loadReadOnly()
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
m.dirty[key] = e
}
actual, loaded, _ = e.tryLoadOrStore(value)
} else if e, ok := m.dirty[key]; ok {
actual, loaded, _ = e.tryLoadOrStore(value)
m.missLocked()
} else {
if !read.amended {
// We're adding the first new key to the dirty map.
// Make sure it is allocated and mark the read-only map as incomplete.
m.dirtyLocked()
m.read.Store(&readOnly[K, V]{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
actual, loaded = value, false
}
m.mu.Unlock()
return actual, loaded
}
// tryLoadOrStore atomically loads or stores a value if the entry is not
// expunged.
//
// If the entry is expunged, tryLoadOrStore leaves the entry unchanged and
// returns with ok==false.
func (e *entry[V]) tryLoadOrStore(i V) (actual V, loaded, ok bool) {
ptr := atomic.LoadPointer(&e.p)
if ptr == expunged {
return actual, false, false
}
p := (*V)(ptr)
if p != nil {
return *p, true, true
}
// Copy the interface after the first load to make this method more amenable
// to escape analysis: if we hit the "load" path or the entry is expunged, we
// shouldn't bother heap-allocating.
ic := i
for {
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
return i, false, true
}
ptr = atomic.LoadPointer(&e.p)
if ptr == expunged {
return actual, false, false
}
p = (*V)(ptr)
if p != nil {
return *p, true, true
}
}
}
// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
read := m.loadReadOnly()
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
delete(m.dirty, key)
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
if ok {
return e.delete()
}
return value, false
}
// Delete deletes the value for a key.
func (m *Map[K, V]) Delete(key K) {
m.LoadAndDelete(key)
}
func (e *entry[V]) delete() (value V, ok bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return value, false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return *(*V)(p), true
}
}
}
// trySwap swaps a value if the entry has not been expunged.
//
// If the entry is expunged, trySwap returns false and leaves the entry
// unchanged.
func (e *entry[V]) trySwap(i *V) (*V, bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return (*V)(p), true
}
}
}
// Swap swaps the value for a key and returns the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map[K, V]) Swap(key K, value V) (previous V, loaded bool) {
read := m.loadReadOnly()
if e, ok := read.m[key]; ok {
if v, ok := e.trySwap(&value); ok {
if v == nil {
return previous, false
}
return *v, true
}
}
m.mu.Lock()
read = m.loadReadOnly()
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// The entry was previously expunged, which implies that there is a
// non-nil dirty map and this entry is not in it.
m.dirty[key] = e
}
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else if e, ok := m.dirty[key]; ok {
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else {
if !read.amended {
// We're adding the first new key to the dirty map.
// Make sure it is allocated and mark the read-only map as incomplete.
m.dirtyLocked()
m.read.Store(&readOnly[K, V]{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
return previous, loaded
}
// Range calls f sequentially for each key and value present in the map.
// If f returns false, range stops the iteration.
//
// Range does not necessarily correspond to any consistent snapshot of the Map's
// contents: no key will be visited more than once, but if the value for any key
// is stored or deleted concurrently (including by f), Range may reflect any
// mapping for that key from any point during the Range call. Range does not
// block other methods on the receiver; even f itself may call any method on m.
//
// Range may be O(N) with the number of elements in the map even if f returns
// false after a constant number of calls.
func (m *Map[K, V]) Range(f func(key K, value V) bool) {
// We need to be able to iterate over all of the keys that were already
// present at the start of the call to Range.
// If read.amended is false, then read.m satisfies that property without
// requiring us to hold m.mu for a long time.
read := m.loadReadOnly()
if read.amended {
// m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
// (assuming the caller does not break out early), so a call to Range
// amortizes an entire copy of the map: we can promote the dirty copy
// immediately!
m.mu.Lock()
read = m.loadReadOnly()
if read.amended {
read = readOnly[K, V]{m: m.dirty}
copyRead := read
m.read.Store(&copyRead)
m.dirty = nil
m.misses = 0
}
m.mu.Unlock()
}
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
// CompareAndSwap swaps the old and new values for key
// if the value stored in the map is equal to old.
// The old value must be of a comparable type.
func CompareAndSwap[K comparable, V comparable](m *Map[K, V], key K, old, new V) (swapped bool) {
read := m.loadReadOnly()
if e, ok := read.m[key]; ok {
return tryCompareAndSwap(e, old, new)
} else if !read.amended {
return false // No existing value for key.
}
m.mu.Lock()
defer m.mu.Unlock()
read = m.loadReadOnly()
swapped = false
if e, ok := read.m[key]; ok {
swapped = tryCompareAndSwap(e, old, new)
} else if e, ok := m.dirty[key]; ok {
swapped = tryCompareAndSwap(e, old, new)
// We needed to lock mu in order to load the entry for key,
// and the operation didn't change the set of keys in the map
// (so it would be made more efficient by promoting the dirty
// map to read-only).
// Count it as a miss so that we will eventually switch to the
// more efficient steady state.
m.missLocked()
}
return swapped
}
// CompareAndDelete deletes the entry for key if its value is equal to old.
// The old value must be of a comparable type.
//
// If there is no current value for key in the map, CompareAndDelete
// returns false (even if the old value is the zero value of V).
func CompareAndDelete[K comparable, V comparable](m *Map[K, V], key K, old V) (deleted bool) {
read := m.loadReadOnly()
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// Don't delete key from m.dirty: we still need to do the “compare” part
// of the operation. The entry will eventually be expunged when the
// dirty map is promoted to the read map.
//
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
for ok {
ptr := atomic.LoadPointer(&e.p)
if ptr == nil || ptr == expunged {
return false
}
p := (*V)(ptr)
if *p != old {
return false
}
if atomic.CompareAndSwapPointer(&e.p, ptr, nil) {
return true
}
}
return false
}
// tryCompareAndSwap compare the entry with the given old value and swaps
// it with a new value if the entry is equal to the old value, and the entry
// has not been expunged.
//
// If the entry is expunged, tryCompareAndSwap returns false and leaves
// the entry unchanged.
func tryCompareAndSwap[V comparable](e *entry[V], old, new V) bool {
ptr := atomic.LoadPointer(&e.p)
if ptr == nil || ptr == expunged {
return false
}
p := (*V)(ptr)
if *p != old {
return false
}
// Copy the interface after the first load to make this method more amenable
// to escape analysis: if the comparison fails from the start, we shouldn't
// bother heap-allocating an interface value to store.
nc := new
for {
if atomic.CompareAndSwapPointer(&e.p, ptr, unsafe.Pointer(&nc)) {
return true
}
ptr = atomic.LoadPointer(&e.p)
if ptr == nil || ptr == expunged {
return false
}
p = (*V)(ptr)
if *p != old {
return false
}
}
}
func (m *Map[K, V]) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
m.read.Store(&readOnly[K, V]{m: m.dirty})
m.dirty = nil
m.misses = 0
}
func (m *Map[K, V]) dirtyLocked() {
if m.dirty != nil {
return
}
read := m.loadReadOnly()
m.dirty = make(map[K]*entry[V], len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
func (e *entry[V]) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}

View File

@@ -47,7 +47,7 @@
pname = "dmsCli"; pname = "dmsCli";
src = ./core; src = ./core;
vendorHash = "sha256-ZbBRV3HOMxbq25Pt/hArKbuyES3j3bbb2kOiLEkCahA="; vendorHash = "sha256-nc4CvEPfJ6l16/zmhnXr1jqpi6BeSXd3g/51djbEfpQ=";
subPackages = ["cmd/dms"]; subPackages = ["cmd/dms"];

View File

@@ -12,5 +12,9 @@ Singleton {
if (!modal.allowStacking) { if (!modal.allowStacking) {
closeAllModalsExcept(modal) closeAllModalsExcept(modal)
} }
if (!modal.keepPopoutsOpen) {
PopoutManager.closeAllPopouts()
}
TrayMenuManager.closeAllMenus()
} }
} }

View File

@@ -0,0 +1,162 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
id: root
property var currentPopoutsByScreen: ({})
property var currentPopoutTriggers: ({})
function showPopout(popout) {
if (!popout || !popout.screen) return
const screenName = popout.screen.name
for (const otherScreenName in currentPopoutsByScreen) {
const otherPopout = currentPopoutsByScreen[otherScreenName]
if (!otherPopout || otherPopout === popout) continue
if (otherPopout.dashVisible !== undefined) {
otherPopout.dashVisible = false
} else if (otherPopout.notificationHistoryVisible !== undefined) {
otherPopout.notificationHistoryVisible = false
} else {
otherPopout.close()
}
}
currentPopoutsByScreen[screenName] = popout
ModalManager.closeAllModalsExcept(null)
TrayMenuManager.closeAllMenus()
}
function hidePopout(popout) {
if (!popout || !popout.screen) return
const screenName = popout.screen.name
if (currentPopoutsByScreen[screenName] === popout) {
currentPopoutsByScreen[screenName] = null
currentPopoutTriggers[screenName] = null
}
}
function closeAllPopouts() {
for (const screenName in currentPopoutsByScreen) {
const popout = currentPopoutsByScreen[screenName]
if (!popout) continue
if (popout.dashVisible !== undefined) {
popout.dashVisible = false
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = false
} else {
popout.close()
}
}
currentPopoutsByScreen = {}
}
function getActivePopout(screen) {
if (!screen) return null
return currentPopoutsByScreen[screen.name] || null
}
function requestPopout(popout, tabIndex, triggerSource) {
if (!popout || !popout.screen) return
const screenName = popout.screen.name
const currentPopout = currentPopoutsByScreen[screenName]
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex
let justClosedSamePopout = false
for (const otherScreenName in currentPopoutsByScreen) {
if (otherScreenName === screenName) continue
const otherPopout = currentPopoutsByScreen[otherScreenName]
if (!otherPopout) continue
if (otherPopout === popout) {
justClosedSamePopout = true
}
if (otherPopout.dashVisible !== undefined) {
otherPopout.dashVisible = false
} else if (otherPopout.notificationHistoryVisible !== undefined) {
otherPopout.notificationHistoryVisible = false
} else {
otherPopout.close()
}
}
if (currentPopout && currentPopout !== popout) {
if (currentPopout.dashVisible !== undefined) {
currentPopout.dashVisible = false
} else if (currentPopout.notificationHistoryVisible !== undefined) {
currentPopout.notificationHistoryVisible = false
} else {
currentPopout.close()
}
}
if (currentPopout === popout && popout.shouldBeVisible) {
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
if (popout.dashVisible !== undefined) {
popout.dashVisible = false
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = false
} else {
popout.close()
}
return
}
if (triggerId === undefined) {
if (popout.dashVisible !== undefined) {
popout.dashVisible = false
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = false
} else {
popout.close()
}
return
}
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex
}
currentPopoutTriggers[screenName] = triggerId
return
}
currentPopoutTriggers[screenName] = triggerId
currentPopoutsByScreen[screenName] = popout
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex
}
ModalManager.closeAllModalsExcept(null)
TrayMenuManager.closeAllMenus()
if (justClosedSamePopout) {
Qt.callLater(() => {
if (popout.dashVisible !== undefined) {
popout.dashVisible = true
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = true
} else {
popout.open()
}
})
} else {
if (popout.dashVisible !== undefined) {
popout.dashVisible = true
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = true
} else {
popout.open()
}
}
}
}

View File

@@ -305,6 +305,12 @@ Singleton {
property int notificationPopupPosition: SettingsData.Position.Top property int notificationPopupPosition: SettingsData.Position.Top
property bool osdAlwaysShowValue: false property bool osdAlwaysShowValue: false
property bool osdVolumeEnabled: true
property bool osdBrightnessEnabled: true
property bool osdIdleInhibitorEnabled: true
property bool osdMicMuteEnabled: true
property bool osdCapsLockEnabled: true
property bool osdPowerProfileEnabled: true
property bool powerActionConfirm: true property bool powerActionConfirm: true
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
@@ -584,6 +590,53 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
} }
} }
function getBarBounds(screen, barThickness) {
if (!screen) {
return { "x": 0, "y": 0, "width": 0, "height": 0, "wingSize": 0 }
}
const wingRadius = dankBarGothCornerRadiusOverride ? dankBarGothCornerRadiusValue : Theme.cornerRadius
const wingSize = dankBarGothCornersEnabled ? Math.max(0, wingRadius) : 0
const screenWidth = screen.width
const screenHeight = screen.height
if (dankBarPosition === SettingsData.Position.Top) {
return {
"x": 0,
"y": 0,
"width": screenWidth,
"height": barThickness + dankBarSpacing + wingSize,
"wingSize": wingSize
}
} else if (dankBarPosition === SettingsData.Position.Bottom) {
return {
"x": 0,
"y": screenHeight - barThickness - dankBarSpacing - wingSize,
"width": screenWidth,
"height": barThickness + dankBarSpacing + wingSize,
"wingSize": wingSize
}
} else if (dankBarPosition === SettingsData.Position.Left) {
return {
"x": 0,
"y": 0,
"width": barThickness + dankBarSpacing + wingSize,
"height": screenHeight,
"wingSize": wingSize
}
} else if (dankBarPosition === SettingsData.Position.Right) {
return {
"x": screenWidth - barThickness - dankBarSpacing - wingSize,
"y": 0,
"width": barThickness + dankBarSpacing + wingSize,
"height": screenHeight,
"wingSize": wingSize
}
}
return { "x": 0, "y": 0, "width": 0, "height": 0, "wingSize": 0 }
}
function getFilteredScreens(componentId) { function getFilteredScreens(componentId) {
var prefs = screenPreferences && screenPreferences[componentId] || ["all"] var prefs = screenPreferences && screenPreferences[componentId] || ["all"]
if (prefs.includes("all")) { if (prefs.includes("all")) {

View File

@@ -0,0 +1,32 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
id: root
property var activeTrayBars: ({})
function register(screenName, trayBar) {
if (!screenName || !trayBar) return
activeTrayBars[screenName] = trayBar
}
function unregister(screenName) {
if (!screenName) return
delete activeTrayBars[screenName]
}
function closeAllMenus() {
for (const screenName in activeTrayBars) {
const trayBar = activeTrayBars[screenName]
if (!trayBar) continue
trayBar.menuOpen = false
if (trayBar.currentTrayMenu) {
trayBar.currentTrayMenu.showMenu = false
}
}
}
}

View File

@@ -215,6 +215,12 @@ var SPEC = {
notificationPopupPosition: { def: 0 }, notificationPopupPosition: { def: 0 },
osdAlwaysShowValue: { def: false }, osdAlwaysShowValue: { def: false },
osdVolumeEnabled: { def: true },
osdBrightnessEnabled: { def: true },
osdIdleInhibitorEnabled: { def: true },
osdMicMuteEnabled: { def: true },
osdCapsLockEnabled: { def: true },
osdPowerProfileEnabled: { def: true },
powerActionConfirm: { def: true }, powerActionConfirm: { def: true },
powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] }, powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] },

View File

@@ -43,6 +43,7 @@ PanelWindow {
property bool allowFocusOverride: false property bool allowFocusOverride: false
property bool allowStacking: false property bool allowStacking: false
property bool keepContentLoaded: false property bool keepContentLoaded: false
property bool keepPopoutsOpen: false
signal opened signal opened
signal dialogClosed signal dialogClosed
@@ -88,7 +89,17 @@ PanelWindow {
} }
} }
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: {
if (!shouldHaveFocus) return WlrKeyboardFocus.None
if (CompositorService.isHyprland) return WlrKeyboardFocus.OnDemand
return WlrKeyboardFocus.Exclusive
}
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && shouldHaveFocus
}
onVisibleChanged: { onVisibleChanged: {
if (root.visible) { if (root.visible) {
opened() opened()

View File

@@ -33,7 +33,9 @@ DankModal {
parentBounds = bounds parentBounds = bounds
parentScreen = targetScreen parentScreen = targetScreen
backgroundOpacity = 0 backgroundOpacity = 0
keepPopoutsOpen = true
open() open()
keepPopoutsOpen = false
} }
function updateVisibleActions() { function updateVisibleActions() {

View File

@@ -139,7 +139,7 @@ Rectangle {
} }
StyledText { StyledText {
text: DgopService.distribution || "Linux" text: UserInfoService.hostname || "Linux"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight

View File

@@ -12,9 +12,9 @@ FocusScope {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: 0 anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS anchors.rightMargin: 0
anchors.bottomMargin: Theme.spacingM anchors.bottomMargin: 0
anchors.topMargin: 0 anchors.topMargin: 0
color: "transparent" color: "transparent"

View File

@@ -66,7 +66,7 @@ Rectangle {
Column { Column {
id: sidebarColumn id: sidebarColumn
width: parent.width anchors.fill: parent
anchors.leftMargin: Theme.spacingS anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS anchors.bottomMargin: Theme.spacingS
@@ -100,7 +100,7 @@ Rectangle {
property bool isActive: sidebarContainer.currentIndex === index property bool isActive: sidebarContainer.currentIndex === index
width: sidebarColumn.width - Theme.spacingS * 2 width: parent.width
height: 44 height: 44
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent" color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"

View File

@@ -17,9 +17,6 @@ DankPopout {
property var triggerScreen: null property var triggerScreen: null
// Setting to Exclusive, so virtual keyboards can send input to app drawer
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
function show() { function show() {
open() open()
} }
@@ -40,6 +37,8 @@ DankPopout {
positioning: "" positioning: ""
screen: triggerScreen screen: triggerScreen
onBackgroundClicked: close()
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (shouldBeVisible) { if (shouldBeVisible) {
appLauncher.searchQuery = "" appLauncher.searchQuery = ""

View File

@@ -35,6 +35,10 @@ Variants {
color: "transparent" color: "transparent"
mask: Region {
item: Item {}
}
Item { Item {
id: root id: root
anchors.fill: parent anchors.fill: parent

View File

@@ -71,7 +71,8 @@ DankPopout {
positioning: "" positioning: ""
screen: triggerScreen screen: triggerScreen
shouldBeVisible: false shouldBeVisible: false
visible: shouldBeVisible
onBackgroundClicked: close()
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (shouldBeVisible) { if (shouldBeVisible) {

View File

@@ -24,6 +24,25 @@ Item {
debounceTimer.restart() debounceTimer.restart()
} }
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
z: -999
onClicked: {
const activePopout = PopoutManager.getActivePopout(barWindow.screen)
if (activePopout) {
if (activePopout.dashVisible !== undefined) {
activePopout.dashVisible = false
} else if (activePopout.notificationHistoryVisible !== undefined) {
activePopout.notificationHistoryVisible = false
} else {
activePopout.close()
}
}
TrayMenuManager.closeAllMenus()
}
}
Timer { Timer {
id: debounceTimer id: debounceTimer
interval: 50 interval: 50

View File

@@ -127,10 +127,7 @@ Item {
dankDashPopoutLoader.item.triggerScreen = barWindow.screen dankDashPopoutLoader.item.triggerScreen = barWindow.screen
} }
if (!dankDashPopoutLoader.item.dashVisible) { PopoutManager.requestPopout(dankDashPopoutLoader.item, 2)
dankDashPopoutLoader.item.currentTabIndex = 2
}
dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible
} }
readonly property var dBarLayer: { readonly property var dBarLayer: {
@@ -1061,7 +1058,9 @@ Item {
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, launcherButton.visualWidth) const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, launcherButton.visualWidth)
appDrawerLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, launcherButton.section, currentScreen) appDrawerLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, launcherButton.section, currentScreen)
} }
appDrawerLoader.item?.toggle() if (appDrawerLoader.item) {
PopoutManager.requestPopout(appDrawerLoader.item, undefined, "appDrawer")
}
} }
} }
} }
@@ -1130,8 +1129,14 @@ Item {
onClockClicked: { onClockClicked: {
dankDashPopoutLoader.active = true dankDashPopoutLoader.active = true
if (dankDashPopoutLoader.item) { if (dankDashPopoutLoader.item) {
dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible if (dankDashPopoutLoader.item.setTriggerPosition) {
dankDashPopoutLoader.item.currentTabIndex = 0 const globalPos = visualContent.mapToGlobal(0, 0)
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, visualWidth)
dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen)
} else {
dankDashPopoutLoader.item.triggerScreen = barWindow.screen
}
PopoutManager.requestPopout(dankDashPopoutLoader.item, 0)
} }
} }
} }
@@ -1154,8 +1159,14 @@ Item {
onClicked: { onClicked: {
dankDashPopoutLoader.active = true dankDashPopoutLoader.active = true
if (dankDashPopoutLoader.item) { if (dankDashPopoutLoader.item) {
dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible if (dankDashPopoutLoader.item.setTriggerPosition) {
dankDashPopoutLoader.item.currentTabIndex = 1 const globalPos = visualContent.mapToGlobal(0, 0)
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, visualWidth)
dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen)
} else {
dankDashPopoutLoader.item.triggerScreen = barWindow.screen
}
PopoutManager.requestPopout(dankDashPopoutLoader.item, 1)
} }
} }
} }
@@ -1177,8 +1188,14 @@ Item {
onClicked: { onClicked: {
dankDashPopoutLoader.active = true dankDashPopoutLoader.active = true
if (dankDashPopoutLoader.item) { if (dankDashPopoutLoader.item) {
dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible if (dankDashPopoutLoader.item.setTriggerPosition) {
dankDashPopoutLoader.item.currentTabIndex = 3 const globalPos = visualContent.mapToGlobal(0, 0)
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, visualWidth)
dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen)
} else {
dankDashPopoutLoader.item.triggerScreen = barWindow.screen
}
PopoutManager.requestPopout(dankDashPopoutLoader.item, 3)
} }
} }
} }
@@ -1323,7 +1340,9 @@ Item {
parentScreen: barWindow.screen parentScreen: barWindow.screen
onClicked: { onClicked: {
notificationCenterLoader.active = true notificationCenterLoader.active = true
notificationCenterLoader.item?.toggle() if (notificationCenterLoader.item) {
PopoutManager.requestPopout(notificationCenterLoader.item, undefined, "notifications")
}
} }
} }
} }
@@ -1344,7 +1363,9 @@ Item {
parentScreen: barWindow.screen parentScreen: barWindow.screen
onToggleBatteryPopup: { onToggleBatteryPopup: {
batteryPopoutLoader.active = true batteryPopoutLoader.active = true
batteryPopoutLoader.item?.toggle() if (batteryPopoutLoader.item) {
PopoutManager.requestPopout(batteryPopoutLoader.item, undefined, "battery")
}
} }
} }
} }
@@ -1365,7 +1386,9 @@ Item {
parentScreen: barWindow.screen parentScreen: barWindow.screen
onToggleLayoutPopup: { onToggleLayoutPopup: {
layoutPopoutLoader.active = true layoutPopoutLoader.active = true
layoutPopoutLoader.item?.toggle() if (layoutPopoutLoader.item) {
PopoutManager.requestPopout(layoutPopoutLoader.item, undefined, "layout")
}
} }
} }
} }
@@ -1385,7 +1408,9 @@ Item {
parentScreen: barWindow.screen parentScreen: barWindow.screen
onToggleVpnPopup: { onToggleVpnPopup: {
vpnPopoutLoader.active = true vpnPopoutLoader.active = true
vpnPopoutLoader.item?.toggle() if (vpnPopoutLoader.item) {
PopoutManager.requestPopout(vpnPopoutLoader.item, undefined, "vpn")
}
} }
} }
} }
@@ -1422,7 +1447,7 @@ Item {
return return
} }
controlCenterLoader.item.triggerScreen = barWindow.screen controlCenterLoader.item.triggerScreen = barWindow.screen
controlCenterLoader.item.toggle() PopoutManager.requestPopout(controlCenterLoader.item, undefined, "controlCenter")
if (controlCenterLoader.item.shouldBeVisible && NetworkService.wifiEnabled) { if (controlCenterLoader.item.shouldBeVisible && NetworkService.wifiEnabled) {
NetworkService.scanWifi() NetworkService.scanWifi()
} }

View File

@@ -50,8 +50,8 @@ DankPopout {
triggerWidth: 70 triggerWidth: 70
positioning: "" positioning: ""
screen: triggerScreen screen: triggerScreen
shouldBeVisible: false
visible: shouldBeVisible onBackgroundClicked: close()
content: Component { content: Component {
Rectangle { Rectangle {

View File

@@ -95,7 +95,6 @@ DankPopout {
positioning: "" positioning: ""
screen: triggerScreen screen: triggerScreen
shouldBeVisible: false shouldBeVisible: false
visible: shouldBeVisible
content: Component { content: Component {
Rectangle { Rectangle {

View File

@@ -46,7 +46,8 @@ DankPopout {
positioning: "" positioning: ""
screen: triggerScreen screen: triggerScreen
shouldBeVisible: false shouldBeVisible: false
visible: shouldBeVisible
onBackgroundClicked: close()
content: Component { content: Component {
Rectangle { Rectangle {

View File

@@ -132,8 +132,8 @@ BasePill {
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen) popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
} }
DgopService.setSortBy("cpu"); DgopService.setSortBy("cpu");
if (root.toggleProcessList) { if (popoutTarget) {
root.toggleProcessList(); PopoutManager.requestPopout(popoutTarget, undefined, "cpu");
} }
} }
} }

View File

@@ -132,8 +132,8 @@ BasePill {
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen) popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
} }
DgopService.setSortBy("cpu"); DgopService.setSortBy("cpu");
if (root.toggleProcessList) { if (popoutTarget) {
root.toggleProcessList(); PopoutManager.requestPopout(popoutTarget, undefined, "cpu_temp");
} }
} }
} }

View File

@@ -196,8 +196,8 @@ BasePill {
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen) popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
} }
DgopService.setSortBy("cpu"); DgopService.setSortBy("cpu");
if (root.toggleProcessList) { if (popoutTarget) {
root.toggleProcessList(); PopoutManager.requestPopout(popoutTarget, undefined, "gpu_temp");
} }
} }
} }

View File

@@ -155,8 +155,8 @@ BasePill {
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen) popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
} }
DgopService.setSortBy("memory"); DgopService.setSortBy("memory");
if (root.toggleProcessList) { if (popoutTarget) {
root.toggleProcessList(); PopoutManager.requestPopout(popoutTarget, undefined, "memory");
} }
} }
} }

View File

@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets import Quickshell.Widgets
@@ -49,7 +50,17 @@ Item {
visible: allTrayItems.length > 0 visible: allTrayItems.length > 0
property bool menuOpen: false property bool menuOpen: false
property bool overflowWasOpenBeforeTrayMenu: false property var currentTrayMenu: null
Component.onCompleted: {
if (!parentScreen) return
TrayMenuManager.register(parentScreen.name, root)
}
Component.onDestruction: {
if (!parentScreen) return
TrayMenuManager.unregister(parentScreen.name)
}
Rectangle { Rectangle {
id: visualBackground id: visualBackground
@@ -171,7 +182,7 @@ Item {
if (!delegateRoot.trayItem.hasMenu) return if (!delegateRoot.trayItem.hasMenu) return
root.overflowWasOpenBeforeTrayMenu = root.menuOpen root.menuOpen = false
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVertical, root.axis) root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVertical, root.axis)
} }
} }
@@ -304,7 +315,7 @@ Item {
if (!delegateRoot.trayItem.hasMenu) return if (!delegateRoot.trayItem.hasMenu) return
root.overflowWasOpenBeforeTrayMenu = root.menuOpen root.menuOpen = false
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVertical, root.axis) root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVertical, root.axis)
} }
} }
@@ -356,10 +367,19 @@ Item {
screen: root.parentScreen screen: root.parentScreen
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand WlrLayershell.keyboardFocus: {
if (!root.menuOpen) return WlrKeyboardFocus.None
if (CompositorService.isHyprland) return WlrKeyboardFocus.OnDemand
return WlrKeyboardFocus.Exclusive
}
WlrLayershell.namespace: "dms:tray-overflow-menu" WlrLayershell.namespace: "dms:tray-overflow-menu"
color: "transparent" color: "transparent"
HyprlandFocusGrab {
windows: [overflowMenu]
active: CompositorService.isHyprland && root.menuOpen
}
anchors { anchors {
top: true top: true
left: true left: true
@@ -372,10 +392,105 @@ Item {
: (screen?.devicePixelRatio || 1) : (screen?.devicePixelRatio || 1)
property point anchorPos: Qt.point(screen.width / 2, screen.height / 2) property point anchorPos: Qt.point(screen.width / 2, screen.height / 2)
readonly property var barBounds: {
if (!overflowMenu.screen) {
return { "x": 0, "y": 0, "width": 0, "height": 0, "wingSize": 0 }
}
return SettingsData.getBarBounds(overflowMenu.screen, root.barThickness + SettingsData.dankBarSpacing)
}
readonly property real barX: barBounds.x
readonly property real barY: barBounds.y
readonly property real barWidth: barBounds.width
readonly property real barHeight: barBounds.height
readonly property real maskX: {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Left:
return barWidth > 0 ? barWidth : 0
case SettingsData.Position.Right:
case SettingsData.Position.Top:
case SettingsData.Position.Bottom:
default:
return 0
}
}
readonly property real maskY: {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Top:
return barHeight > 0 ? barHeight : 0
case SettingsData.Position.Bottom:
case SettingsData.Position.Left:
case SettingsData.Position.Right:
default:
return 0
}
}
readonly property real maskWidth: {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Left:
return barWidth > 0 ? width - barWidth : width
case SettingsData.Position.Right:
return barWidth > 0 ? width - barWidth : width
case SettingsData.Position.Top:
case SettingsData.Position.Bottom:
default:
return width
}
}
readonly property real maskHeight: {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Top:
return barHeight > 0 ? height - barHeight : height
case SettingsData.Position.Bottom:
return barHeight > 0 ? height - barHeight : height
case SettingsData.Position.Left:
case SettingsData.Position.Right:
default:
return height
}
}
mask: Region {
item: Rectangle {
x: overflowMenu.maskX
y: overflowMenu.maskY
width: overflowMenu.maskWidth
height: overflowMenu.maskHeight
}
}
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
if (currentTrayMenu) {
currentTrayMenu.showMenu = false
}
PopoutManager.closeAllPopouts()
ModalManager.closeAllModalsExcept(null)
updatePosition() updatePosition()
Qt.callLater(() => overflowFocusScope.forceActiveFocus()) }
}
MouseArea {
x: overflowMenu.maskX
y: overflowMenu.maskY
width: overflowMenu.maskWidth
height: overflowMenu.maskHeight
z: -1
enabled: root.menuOpen
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
const clickX = mouse.x + overflowMenu.maskX
const clickY = mouse.y + overflowMenu.maskY
const outsideContent = clickX < menuContainer.x || clickX > menuContainer.x + menuContainer.width ||
clickY < menuContainer.y || clickY > menuContainer.y + menuContainer.height
if (!outsideContent) return
root.menuOpen = false
} }
} }
@@ -614,7 +729,6 @@ Item {
if (!trayItem.hasMenu) return if (!trayItem.hasMenu) return
root.overflowWasOpenBeforeTrayMenu = true
root.menuOpen = false root.menuOpen = false
root.showForTrayItem(trayItem, parent, parentScreen, root.isAtBottom, root.isVertical, root.axis) root.showForTrayItem(trayItem, parent, parentScreen, root.isAtBottom, root.isVertical, root.axis)
} }
@@ -623,12 +737,6 @@ Item {
} }
} }
} }
MouseArea {
anchors.fill: parent
z: -1
onClicked: root.menuOpen = false
}
} }
Component { Component {
@@ -675,19 +783,9 @@ Item {
function close() { function close() {
showMenu = false showMenu = false
if (root.overflowWasOpenBeforeTrayMenu) {
root.menuOpen = true
Qt.callLater(() => {
if (overflowMenu.visible && overflowFocusScope) {
overflowFocusScope.forceActiveFocus()
}
})
}
root.overflowWasOpenBeforeTrayMenu = false
} }
function closeWithAction() { function closeWithAction() {
root.overflowWasOpenBeforeTrayMenu = false
close() close()
} }
@@ -721,9 +819,18 @@ Item {
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false) visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand WlrLayershell.keyboardFocus: {
if (!menuRoot.showMenu) return WlrKeyboardFocus.None
if (CompositorService.isHyprland) return WlrKeyboardFocus.OnDemand
return WlrKeyboardFocus.Exclusive
}
color: "transparent" color: "transparent"
HyprlandFocusGrab {
windows: [menuWindow]
active: CompositorService.isHyprland && menuRoot.showMenu
}
anchors { anchors {
top: true top: true
left: true left: true
@@ -736,10 +843,103 @@ Item {
: (screen?.devicePixelRatio || 1) : (screen?.devicePixelRatio || 1)
property point anchorPos: Qt.point(screen.width / 2, screen.height / 2) property point anchorPos: Qt.point(screen.width / 2, screen.height / 2)
readonly property var barBounds: {
if (!menuWindow.screen) {
return { "x": 0, "y": 0, "width": 0, "height": 0, "wingSize": 0 }
}
return SettingsData.getBarBounds(menuWindow.screen, root.barThickness + SettingsData.dankBarSpacing)
}
readonly property real barX: barBounds.x
readonly property real barY: barBounds.y
readonly property real barWidth: barBounds.width
readonly property real barHeight: barBounds.height
readonly property real maskX: {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Left:
return barWidth > 0 ? barWidth : 0
case SettingsData.Position.Right:
case SettingsData.Position.Top:
case SettingsData.Position.Bottom:
default:
return 0
}
}
readonly property real maskY: {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Top:
return barHeight > 0 ? barHeight : 0
case SettingsData.Position.Bottom:
case SettingsData.Position.Left:
case SettingsData.Position.Right:
default:
return 0
}
}
readonly property real maskWidth: {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Left:
return barWidth > 0 ? width - barWidth : width
case SettingsData.Position.Right:
return barWidth > 0 ? width - barWidth : width
case SettingsData.Position.Top:
case SettingsData.Position.Bottom:
default:
return width
}
}
readonly property real maskHeight: {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Top:
return barHeight > 0 ? height - barHeight : height
case SettingsData.Position.Bottom:
return barHeight > 0 ? height - barHeight : height
case SettingsData.Position.Left:
case SettingsData.Position.Right:
default:
return height
}
}
mask: Region {
item: Rectangle {
x: menuWindow.maskX
y: menuWindow.maskY
width: menuWindow.maskWidth
height: menuWindow.maskHeight
}
}
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
root.menuOpen = false
PopoutManager.closeAllPopouts()
ModalManager.closeAllModalsExcept(null)
updatePosition() updatePosition()
Qt.callLater(() => menuFocusScope.forceActiveFocus()) }
}
MouseArea {
x: menuWindow.maskX
y: menuWindow.maskY
width: menuWindow.maskWidth
height: menuWindow.maskHeight
z: -1
enabled: menuRoot.showMenu
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
const clickX = mouse.x + menuWindow.maskX
const clickY = mouse.y + menuWindow.maskY
const outsideContent = clickX < menuContainer.x || clickX > menuContainer.x + menuContainer.width ||
clickY < menuContainer.y || clickY > menuContainer.y + menuContainer.height
if (!outsideContent) return
menuRoot.close()
} }
} }
@@ -1095,7 +1295,7 @@ Item {
width: 16 width: 16
height: 16 height: 16
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: menuEntry?.icon && menuEntry.icon !== "" visible: (menuEntry?.icon ?? "") !== ""
Image { Image {
anchors.fill: parent anchors.fill: parent
@@ -1135,40 +1335,23 @@ Item {
} }
} }
} }
MouseArea {
anchors.fill: parent
z: -1
onClicked: menuRoot.close()
}
}
}
}
property var currentTrayMenu: null
Connections {
target: currentTrayMenu
enabled: currentTrayMenu !== null
function onShowMenuChanged() {
if (parentWindow && typeof parentWindow.systemTrayMenuOpen !== "undefined") {
parentWindow.systemTrayMenuOpen = currentTrayMenu.showMenu
} }
} }
} }
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) { function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
if (parentWindow && typeof parentWindow.systemTrayMenuOpen !== "undefined") { if (!screen) return
parentWindow.systemTrayMenuOpen = true
}
if (currentTrayMenu) { if (currentTrayMenu) {
currentTrayMenu.showMenu = false
currentTrayMenu.destroy() currentTrayMenu.destroy()
currentTrayMenu = null
} }
currentTrayMenu = trayMenuComponent.createObject(null) currentTrayMenu = trayMenuComponent.createObject(null)
if (currentTrayMenu) { if (!currentTrayMenu) return
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj)
} currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj)
} }
} }

View File

@@ -18,7 +18,6 @@ DankPopout {
property var triggerScreen: null property var triggerScreen: null
property int currentTabIndex: 0 property int currentTabIndex: 0
keyboardFocusMode: WlrKeyboardFocus.Exclusive
function setTriggerPosition(x, y, width, section, screen) { function setTriggerPosition(x, y, width, section, screen) {
triggerSection = section triggerSection = section
@@ -44,8 +43,8 @@ DankPopout {
triggerX: Screen.width - 620 - Theme.spacingL triggerX: Screen.width - 620 - Theme.spacingL
triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2 triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2
triggerWidth: 80 triggerWidth: 80
screen: triggerScreen
shouldBeVisible: dashVisible shouldBeVisible: dashVisible
visible: shouldBeVisible
property bool __focusArmed: false property bool __focusArmed: false
property bool __contentReady: false property bool __contentReady: false

View File

@@ -1086,9 +1086,9 @@ Item {
}) })
} }
// 1. Explicit system/user paths // 1. Explicit system/user paths
var explicitFind = "find " + paths.join(" ") + " -maxdepth 1 -name '*.desktop' -type f 2>/dev/null" var explicitFind = "find " + paths.join(" ") + " -maxdepth 1 -name '*.desktop' -type f -follow 2>/dev/null"
// 2. Scan all /home user directories for local session files // 2. Scan all /home user directories for local session files
var homeScan = "find /home -maxdepth 5 \\( -path '*/wayland-sessions/*.desktop' -o -path '*/xsessions/*.desktop' \\) -type f 2>/dev/null" var homeScan = "find /home -maxdepth 5 \\( -path '*/wayland-sessions/*.desktop' -o -path '*/xsessions/*.desktop' \\) -type f -follow 2>/dev/null"
var findCmd = "(" + explicitFind + "; " + homeScan + ") | sort -u" var findCmd = "(" + explicitFind + "; " + homeScan + ") | sort -u"
return ["sh", "-c", findCmd] return ["sh", "-c", findCmd]
} }

View File

@@ -35,7 +35,7 @@ Scope {
WlrLayershell.namespace: "dms:workspace-overview" WlrLayershell.namespace: "dms:workspace-overview"
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive WlrLayershell.keyboardFocus: overviewScope.overviewOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
anchors { anchors {
top: true top: true

View File

@@ -42,7 +42,14 @@ DankPopout {
positioning: "" positioning: ""
screen: triggerScreen screen: triggerScreen
shouldBeVisible: notificationHistoryVisible shouldBeVisible: notificationHistoryVisible
visible: shouldBeVisible
function toggle() {
notificationHistoryVisible = !notificationHistoryVisible
}
onBackgroundClicked: {
notificationHistoryVisible = false
}
onNotificationHistoryVisibleChanged: { onNotificationHistoryVisibleChanged: {
if (notificationHistoryVisible) { if (notificationHistoryVisible) {

View File

@@ -14,7 +14,7 @@ DankOSD {
Connections { Connections {
target: DisplayService target: DisplayService
function onBrightnessChanged(showOsd) { function onBrightnessChanged(showOsd) {
if (showOsd) { if (showOsd && SettingsData.osdBrightnessEnabled) {
root.show() root.show()
} }
} }

View File

@@ -17,7 +17,7 @@ DankOSD {
target: DMSService target: DMSService
function onCapsLockStateChanged() { function onCapsLockStateChanged() {
if (lastCapsLockState !== DMSService.capsLockState) { if (lastCapsLockState !== DMSService.capsLockState && SettingsData.osdCapsLockEnabled) {
root.show() root.show()
} }
lastCapsLockState = DMSService.capsLockState lastCapsLockState = DMSService.capsLockState

View File

@@ -14,7 +14,9 @@ DankOSD {
Connections { Connections {
target: SessionService target: SessionService
function onInhibitorChanged() { function onInhibitorChanged() {
root.show() if (SettingsData.osdIdleInhibitorEnabled) {
root.show()
}
} }
} }

View File

@@ -14,7 +14,9 @@ DankOSD {
Connections { Connections {
target: AudioService target: AudioService
function onMicMuteChanged() { function onMicMuteChanged() {
root.show() if (SettingsData.osdMicMuteEnabled) {
root.show()
}
} }
} }

View File

@@ -18,7 +18,7 @@ DankOSD {
target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null
function onProfileChanged() { function onProfileChanged() {
if (lastProfile !== -1 && lastProfile !== PowerProfiles.profile) { if (lastProfile !== -1 && lastProfile !== PowerProfiles.profile && SettingsData.osdPowerProfileEnabled) {
root.show() root.show()
} }
lastProfile = PowerProfiles.profile lastProfile = PowerProfiles.profile

View File

@@ -15,13 +15,13 @@ DankOSD {
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
function onVolumeChanged() { function onVolumeChanged() {
if (!AudioService.suppressOSD) { if (!AudioService.suppressOSD && SettingsData.osdVolumeEnabled) {
root.show() root.show()
} }
} }
function onMutedChanged() { function onMutedChanged() {
if (!AudioService.suppressOSD) { if (!AudioService.suppressOSD && SettingsData.osdVolumeEnabled) {
root.show() root.show()
} }
} }
@@ -31,7 +31,7 @@ DankOSD {
target: AudioService target: AudioService
function onSinkChanged() { function onSinkChanged() {
if (root.shouldBeVisible) { if (root.shouldBeVisible && SettingsData.osdVolumeEnabled) {
root.show() root.show()
} }
} }

View File

@@ -8,8 +8,6 @@ DankPopout {
layerNamespace: "dms-plugin:" + layerNamespacePlugin layerNamespace: "dms-plugin:" + layerNamespacePlugin
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
property var triggerScreen: null property var triggerScreen: null
property Component pluginContent: null property Component pluginContent: null
property real contentWidth: 400 property real contentWidth: 400
@@ -27,7 +25,6 @@ DankPopout {
popupHeight: contentHeight popupHeight: contentHeight
screen: triggerScreen screen: triggerScreen
shouldBeVisible: false shouldBeVisible: false
visible: shouldBeVisible
content: Component { content: Component {
Rectangle { Rectangle {

View File

@@ -45,9 +45,10 @@ DankPopout {
triggerWidth: 55 triggerWidth: 55
positioning: "" positioning: ""
screen: triggerScreen screen: triggerScreen
visible: shouldBeVisible
shouldBeVisible: false shouldBeVisible: false
onBackgroundClicked: close()
Ref { Ref {
service: DgopService service: DgopService
} }

View File

@@ -9,10 +9,10 @@ DankFlickable {
contentHeight: systemColumn.implicitHeight contentHeight: systemColumn.implicitHeight
clip: true clip: true
Component.onCompleted: { Component.onCompleted: {
DgopService.addRef(["system", "hardware", "diskmounts"]); DgopService.addRef(["system", "diskmounts"]);
} }
Component.onDestruction: { Component.onDestruction: {
DgopService.removeRef(["system", "hardware", "diskmounts"]); DgopService.removeRef(["system", "diskmounts"]);
} }
Column { Column {

View File

@@ -75,7 +75,6 @@ Item {
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true clip: true
contentHeight: mainColumn.height contentHeight: mainColumn.height
contentWidth: width contentWidth: width

View File

@@ -736,8 +736,6 @@ Item {
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingS
clip: true clip: true
contentHeight: mainColumn.height contentHeight: mainColumn.height
contentWidth: width contentWidth: width

Some files were not shown because too many files have changed in this diff Show More