1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-25 05:52: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/spf13/cobra v1.10.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
)

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/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/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/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=

View File

@@ -1,12 +1,12 @@
// 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
//
// dwl_ipc_unstable_v2 Protocol Copyright:
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].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the

View File

@@ -1,5 +1,5 @@
// 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
//
// ext_workspace_v1 Protocol Copyright:
@@ -35,7 +35,8 @@ import (
"reflect"
"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.
@@ -61,8 +62,9 @@ func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint3
return
}
objectsMap := reflect.NewAt(objectsField.Type(), unsafe.Pointer(objectsField.UnsafeAddr())).Elem()
objectsMap.SetMapIndex(reflect.ValueOf(serverID), reflect.ValueOf(proxy))
objectsMapPtr := unsafe.Pointer(objectsField.UnsafeAddr())
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].

View File

@@ -1,5 +1,5 @@
// 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
//
// wlr_gamma_control_unstable_v1 Protocol Copyright:
@@ -31,7 +31,7 @@
package wlr_gamma_control
import (
"github.com/yaslama/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix"
)

View File

@@ -1,5 +1,5 @@
// 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
//
// wlr_output_management_unstable_v1 Protocol Copyright:
@@ -33,7 +33,8 @@ import (
"reflect"
"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) {
@@ -47,9 +48,9 @@ func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint3
if !objectsField.IsValid() {
return
}
objectsField = reflect.NewAt(objectsField.Type(), unsafe.Pointer(objectsField.UnsafeAddr())).Elem()
objectsMap := objectsField.Interface().(map[uint32]client.Proxy)
objectsMap[serverID] = proxy
objectsMapPtr := unsafe.Pointer(objectsField.UnsafeAddr())
objectsMap := (*syncmap.Map[uint32, client.Proxy])(objectsMapPtr)
objectsMap.Store(serverID, proxy)
}
// 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{},
ConnectedDevices: []Device{},
},
stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan BluetoothState),
subMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
dbusConn: conn,
signals: make(chan *dbus.Signal, 256),
pairingSubscribers: make(map[string]chan PairingPrompt),
pairingSubMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
pendingPairings: make(map[string]bool),
eventQueue: make(chan func(), 32),
stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
dbusConn: conn,
signals: make(chan *dbus.Signal, 256),
dirty: make(chan struct{}, 1),
eventQueue: make(chan func(), 32),
}
broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
@@ -360,12 +356,7 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
if hasPaired {
if paired, ok := pairedVar.Value().(bool); ok && paired {
devicePath := string(path)
m.pendingPairingsMux.Lock()
wasPending := m.pendingPairings[devicePath]
if wasPending {
delete(m.pendingPairings, devicePath)
}
m.pendingPairingsMux.Unlock()
_, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
if wasPending {
select {
@@ -430,28 +421,20 @@ func (m *Manager) notifier() {
}
m.updateDevices()
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false
continue
}
for _, ch := range m.subscribers {
m.subscribers.Range(func(key string, ch chan BluetoothState) bool {
select {
case ch <- currentState:
default:
}
}
m.subMutex.RUnlock()
return true
})
stateCopy := currentState
m.lastNotifiedState = &stateCopy
@@ -484,48 +467,36 @@ func (m *Manager) snapshotState() BluetoothState {
func (m *Manager) Subscribe(id string) chan BluetoothState {
ch := make(chan BluetoothState, 64)
m.subMutex.Lock()
m.subscribers[id] = ch
m.subMutex.Unlock()
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok {
if ch, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch)
delete(m.subscribers, id)
}
m.subMutex.Unlock()
}
func (m *Manager) SubscribePairing(id string) chan PairingPrompt {
ch := make(chan PairingPrompt, 16)
m.pairingSubMutex.Lock()
m.pairingSubscribers[id] = ch
m.pairingSubMutex.Unlock()
m.pairingSubscribers.Store(id, ch)
return ch
}
func (m *Manager) UnsubscribePairing(id string) {
m.pairingSubMutex.Lock()
if ch, ok := m.pairingSubscribers[id]; ok {
if ch, ok := m.pairingSubscribers.LoadAndDelete(id); ok {
close(ch)
delete(m.pairingSubscribers, id)
}
m.pairingSubMutex.Unlock()
}
func (m *Manager) broadcastPairingPrompt(prompt PairingPrompt) {
m.pairingSubMutex.RLock()
defer m.pairingSubMutex.RUnlock()
for _, ch := range m.pairingSubscribers {
m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool {
select {
case ch <- prompt:
default:
}
}
return true
})
}
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 {
m.pendingPairingsMux.Lock()
m.pendingPairings[devicePath] = true
m.pendingPairingsMux.Unlock()
m.pendingPairings.Store(devicePath, true)
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath))
err := obj.Call(device1Iface+".Pair", 0).Err
if err != nil {
m.pendingPairingsMux.Lock()
delete(m.pendingPairings, devicePath)
m.pendingPairingsMux.Unlock()
m.pendingPairings.Delete(devicePath)
}
return err
@@ -618,19 +585,17 @@ func (m *Manager) Close() {
m.agent.Close()
}
m.subMutex.Lock()
for _, ch := range m.subscribers {
m.subscribers.Range(func(key string, ch chan BluetoothState) bool {
close(ch)
}
m.subscribers = make(map[string]chan BluetoothState)
m.subMutex.Unlock()
m.subscribers.Delete(key)
return true
})
m.pairingSubMutex.Lock()
for _, ch := range m.pairingSubscribers {
m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool {
close(ch)
}
m.pairingSubscribers = make(map[string]chan PairingPrompt)
m.pairingSubMutex.Unlock()
m.pairingSubscribers.Delete(key)
return true
})
if m.dbusConn != nil {
m.dbusConn.Close()

View File

@@ -3,22 +3,19 @@ package bluez
import (
"context"
"fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type SubscriptionBroker struct {
mu sync.RWMutex
pending map[string]chan PromptReply
requests map[string]PromptRequest
pending syncmap.Map[string, chan PromptReply]
requests syncmap.Map[string, PromptRequest]
broadcastPrompt func(PairingPrompt)
}
func NewSubscriptionBroker(broadcastPrompt func(PairingPrompt)) PromptBroker {
return &SubscriptionBroker{
pending: make(map[string]chan PromptReply),
requests: make(map[string]PromptRequest),
broadcastPrompt: broadcastPrompt,
}
}
@@ -30,10 +27,8 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
}
replyChan := make(chan PromptReply, 1)
b.mu.Lock()
b.pending[token] = replyChan
b.requests[token] = req
b.mu.Unlock()
b.pending.Store(token, replyChan)
b.requests.Store(token, req)
if b.broadcastPrompt != nil {
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) {
b.mu.RLock()
replyChan, exists := b.pending[token]
b.mu.RUnlock()
replyChan, exists := b.pending.Load(token)
if !exists {
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 {
b.mu.RLock()
replyChan, exists := b.pending[token]
b.mu.RUnlock()
replyChan, exists := b.pending.Load(token)
if !exists {
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) {
b.mu.Lock()
delete(b.pending, token)
delete(b.requests, token)
b.mu.Unlock()
b.pending.Delete(token)
b.requests.Delete(token)
}

View File

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

View File

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

View File

@@ -15,10 +15,8 @@ func NewManager() (*Manager, error) {
func NewManagerWithOptions(exponential bool) (*Manager, error) {
m := &Manager{
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
exponential: exponential,
stopChan: make(chan struct{}),
exponential: exponential,
}
go m.initLogind()
@@ -360,20 +358,13 @@ func (m *Manager) broadcastDeviceUpdate(deviceID string) {
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)
for _, ch := range m.updateSubscribers {
m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool {
select {
case ch <- update:
default:
}
}
return true
})
}

View File

@@ -13,9 +13,8 @@ import (
func NewSysfsBackend() (*SysfsBackend, error) {
b := &SysfsBackend{
basePath: "/sys/class",
classes: []string{"backlight", "leds"},
deviceCache: make(map[string]*sysfsDevice),
basePath: "/sys/class",
classes: []string{"backlight", "leds"},
}
if err := b.scanDevices(); err != nil {
@@ -26,9 +25,6 @@ func NewSysfsBackend() (*SysfsBackend, error) {
}
func (b *SysfsBackend) scanDevices() error {
b.deviceCacheMutex.Lock()
defer b.deviceCacheMutex.Unlock()
for _, class := range b.classes {
classPath := filepath.Join(b.basePath, class)
entries, err := os.ReadDir(classPath)
@@ -68,13 +64,13 @@ func (b *SysfsBackend) scanDevices() error {
}
deviceID := fmt.Sprintf("%s:%s", class, entry.Name())
b.deviceCache[deviceID] = &sysfsDevice{
b.deviceCache.Store(deviceID, &sysfsDevice{
class: deviceClass,
id: deviceID,
name: entry.Name(),
maxBrightness: maxBrightness,
minValue: minValue,
}
})
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) {
b.deviceCacheMutex.RLock()
defer b.deviceCacheMutex.RUnlock()
devices := make([]Device, 0)
devices := make([]Device, 0, len(b.deviceCache))
for _, dev := range b.deviceCache {
b.deviceCache.Range(func(key string, dev *sysfsDevice) bool {
if shouldSuppressDevice(dev.name) {
continue
return true
}
parts := strings.SplitN(dev.id, ":", 2)
if len(parts) != 2 {
continue
return true
}
class := parts[0]
@@ -130,13 +123,13 @@ func (b *SysfsBackend) GetDevices() ([]Device, error) {
brightnessData, err := os.ReadFile(brightnessPath)
if err != nil {
log.Debugf("failed to read brightness for %s: %v", dev.id, err)
continue
return true
}
current, err := strconv.Atoi(strings.TrimSpace(string(brightnessData)))
if err != nil {
log.Debugf("failed to parse brightness for %s: %v", dev.id, err)
continue
return true
}
percent := b.ValueToPercent(current, dev, false)
@@ -150,16 +143,14 @@ func (b *SysfsBackend) GetDevices() ([]Device, error) {
CurrentPercent: percent,
Backend: "sysfs",
})
}
return true
})
return devices, nil
}
func (b *SysfsBackend) GetDevice(id string) (*sysfsDevice, error) {
b.deviceCacheMutex.RLock()
defer b.deviceCacheMutex.RUnlock()
dev, ok := b.deviceCache[id]
dev, ok := b.deviceCache.Load(id)
if !ok {
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)
sysfs := &SysfsBackend{
basePath: tmpDir,
classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
basePath: tmpDir,
classes: []string{"backlight"},
}
if err := sysfs.scanDevices(); err != nil {
@@ -41,13 +40,11 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
}
m := &Manager{
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
stopChan: make(chan struct{}),
}
m.state = State{
@@ -105,9 +102,8 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{
basePath: tmpDir,
classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
basePath: tmpDir,
classes: []string{"backlight"},
}
if err := sysfs.scanDevices(); err != nil {
@@ -115,13 +111,11 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
}
m := &Manager{
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
stopChan: make(chan struct{}),
}
m.state = State{
@@ -175,9 +169,8 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
}
sysfs := &SysfsBackend{
basePath: tmpDir,
classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
basePath: tmpDir,
classes: []string{"backlight"},
}
if err := sysfs.scanDevices(); err != nil {
@@ -185,13 +178,11 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
}
m := &Manager{
logindBackend: nil,
sysfsBackend: sysfs,
logindReady: false,
sysfsReady: true,
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
logindBackend: nil,
sysfsBackend: sysfs,
logindReady: false,
sysfsReady: true,
stopChan: make(chan struct{}),
}
m.state = State{
@@ -240,9 +231,8 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{
basePath: tmpDir,
classes: []string{"leds"},
deviceCache: make(map[string]*sysfsDevice),
basePath: tmpDir,
classes: []string{"leds"},
}
if err := sysfs.scanDevices(); err != nil {
@@ -250,13 +240,11 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
}
m := &Manager{
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
subscribers: make(map[string]chan State),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
stopChan: make(chan struct{}),
}
m.state = State{

View File

@@ -160,26 +160,21 @@ func TestSysfsBackend_ScanDevices(t *testing.T) {
}
b := &SysfsBackend{
basePath: tmpDir,
classes: []string{"backlight", "leds"},
deviceCache: make(map[string]*sysfsDevice),
basePath: tmpDir,
classes: []string{"backlight", "leds"},
}
if err := b.scanDevices(); err != nil {
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"
if _, ok := b.deviceCache[backlightID]; !ok {
if _, ok := b.deviceCache.Load(backlightID); !ok {
t.Errorf("backlight device not found")
}
ledID := "leds:test_led"
if _, ok := b.deviceCache[ledID]; !ok {
if _, ok := b.deviceCache.Load(ledID); !ok {
t.Errorf("LED device not found")
}
}

View File

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

View File

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

View File

@@ -13,10 +13,9 @@ func TestNewManager(t *testing.T) {
state: &CUPSState{
Printers: make(map[string]*Printer),
},
client: nil,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
client: nil,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
}
assert.NotNil(t, m)
@@ -35,10 +34,9 @@ func TestManager_GetState(t *testing.T) {
},
},
},
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
}
state := m.GetState()
@@ -53,18 +51,28 @@ func TestManager_Subscribe(t *testing.T) {
state: &CUPSState{
Printers: make(map[string]*Printer),
},
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
}
ch := m.Subscribe("test-client")
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")
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) {
@@ -74,10 +82,9 @@ func TestManager_Close(t *testing.T) {
state: &CUPSState{
Printers: make(map[string]*Printer),
},
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
client: mockClient,
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
}
m.eventWG.Add(1)
@@ -93,7 +100,12 @@ func TestManager_Close(t *testing.T) {
}()
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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,10 +16,9 @@ func TestManager_Creation(t *testing.T) {
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}),
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
assert.NotNil(t, m)
@@ -32,10 +31,9 @@ func TestManager_Creation(t *testing.T) {
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: true},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}),
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: true},
closeChan: make(chan struct{}),
}
assert.NotNil(t, m)
@@ -52,7 +50,6 @@ func TestManager_GetState(t *testing.T) {
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}),
}
@@ -69,13 +66,17 @@ func TestManager_Subscribe(t *testing.T) {
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}),
}
ch := m.Subscribe("test-client")
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) {
@@ -86,15 +87,24 @@ func TestManager_Unsubscribe(t *testing.T) {
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}),
}
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")
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 {
case _, ok := <-ch:
@@ -112,7 +122,6 @@ func TestManager_UpdateCapsLock(t *testing.T) {
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}),
}
@@ -148,7 +157,6 @@ func TestManager_Close(t *testing.T) {
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}),
}
@@ -171,7 +179,12 @@ func TestManager_Close(t *testing.T) {
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()
}
@@ -194,6 +207,10 @@ func TestIsKeyboard(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t)
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)
assert.Equal(t, tt.expected, result)
})
@@ -226,10 +243,9 @@ func TestManager_MonitorDevice(t *testing.T) {
mockDevice.EXPECT().Close().Return(nil).Maybe()
m := &Manager{
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}),
devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false},
closeChan: make(chan struct{}),
}
ch := m.Subscribe("test")
@@ -272,7 +288,6 @@ func TestNotifySubscribers(t *testing.T) {
devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}),
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -240,19 +240,25 @@ func TestHandleSubscribe(t *testing.T) {
func TestManager_Subscribe_Unsubscribe(t *testing.T) {
manager := &Manager{
state: &NetworkState{},
subscribers: make(map[string]chan NetworkState),
state: &NetworkState{},
}
t.Run("subscribe creates channel", func(t *testing.T) {
ch := manager.Subscribe("client1")
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) {
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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"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 {

View File

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

View File

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

View File

@@ -4,7 +4,8 @@ import (
"sync"
"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 {
@@ -49,11 +50,8 @@ type Manager struct {
registry *wlclient.Registry
manager *wlr_output_management.ZwlrOutputManagerV1
headsMutex sync.RWMutex
heads map[uint32]*headState
modesMutex sync.RWMutex
modes map[uint32]*modeState
heads syncmap.Map[uint32, *headState]
modes syncmap.Map[uint32, *modeState]
serial uint32
@@ -62,8 +60,7 @@ type Manager struct {
stopChan chan struct{}
wg sync.WaitGroup
subscribers map[string]chan State
subMutex sync.RWMutex
subscribers syncmap.Map[string, chan State]
dirty chan struct{}
notifierWg sync.WaitGroup
lastNotified *State
@@ -120,19 +117,19 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch
m.subMutex.Unlock()
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok {
close(ch)
delete(m.subscribers, id)
if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val)
}
m.subMutex.Unlock()
}
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";
src = ./core;
vendorHash = "sha256-ZbBRV3HOMxbq25Pt/hArKbuyES3j3bbb2kOiLEkCahA=";
vendorHash = "sha256-nc4CvEPfJ6l16/zmhnXr1jqpi6BeSXd3g/51djbEfpQ=";
subPackages = ["cmd/dms"];

View File

@@ -12,5 +12,9 @@ Singleton {
if (!modal.allowStacking) {
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 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 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) {
var prefs = screenPreferences && screenPreferences[componentId] || ["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 },
osdAlwaysShowValue: { def: false },
osdVolumeEnabled: { def: true },
osdBrightnessEnabled: { def: true },
osdIdleInhibitorEnabled: { def: true },
osdMicMuteEnabled: { def: true },
osdCapsLockEnabled: { def: true },
osdPowerProfileEnabled: { def: true },
powerActionConfirm: { def: true },
powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] },

View File

@@ -43,6 +43,7 @@ PanelWindow {
property bool allowFocusOverride: false
property bool allowStacking: false
property bool keepContentLoaded: false
property bool keepPopoutsOpen: false
signal opened
signal dialogClosed
@@ -88,7 +89,17 @@ PanelWindow {
}
}
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: {
if (root.visible) {
opened()

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,9 +17,6 @@ DankPopout {
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() {
open()
}
@@ -40,6 +37,8 @@ DankPopout {
positioning: ""
screen: triggerScreen
onBackgroundClicked: close()
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
appLauncher.searchQuery = ""

View File

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

View File

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

View File

@@ -24,6 +24,25 @@ Item {
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 {
id: debounceTimer
interval: 50

View File

@@ -127,10 +127,7 @@ Item {
dankDashPopoutLoader.item.triggerScreen = barWindow.screen
}
if (!dankDashPopoutLoader.item.dashVisible) {
dankDashPopoutLoader.item.currentTabIndex = 2
}
dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible
PopoutManager.requestPopout(dankDashPopoutLoader.item, 2)
}
readonly property var dBarLayer: {
@@ -1061,7 +1058,9 @@ Item {
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, launcherButton.visualWidth)
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: {
dankDashPopoutLoader.active = true
if (dankDashPopoutLoader.item) {
dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible
dankDashPopoutLoader.item.currentTabIndex = 0
if (dankDashPopoutLoader.item.setTriggerPosition) {
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: {
dankDashPopoutLoader.active = true
if (dankDashPopoutLoader.item) {
dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible
dankDashPopoutLoader.item.currentTabIndex = 1
if (dankDashPopoutLoader.item.setTriggerPosition) {
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: {
dankDashPopoutLoader.active = true
if (dankDashPopoutLoader.item) {
dankDashPopoutLoader.item.dashVisible = !dankDashPopoutLoader.item.dashVisible
dankDashPopoutLoader.item.currentTabIndex = 3
if (dankDashPopoutLoader.item.setTriggerPosition) {
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
onClicked: {
notificationCenterLoader.active = true
notificationCenterLoader.item?.toggle()
if (notificationCenterLoader.item) {
PopoutManager.requestPopout(notificationCenterLoader.item, undefined, "notifications")
}
}
}
}
@@ -1344,7 +1363,9 @@ Item {
parentScreen: barWindow.screen
onToggleBatteryPopup: {
batteryPopoutLoader.active = true
batteryPopoutLoader.item?.toggle()
if (batteryPopoutLoader.item) {
PopoutManager.requestPopout(batteryPopoutLoader.item, undefined, "battery")
}
}
}
}
@@ -1365,7 +1386,9 @@ Item {
parentScreen: barWindow.screen
onToggleLayoutPopup: {
layoutPopoutLoader.active = true
layoutPopoutLoader.item?.toggle()
if (layoutPopoutLoader.item) {
PopoutManager.requestPopout(layoutPopoutLoader.item, undefined, "layout")
}
}
}
}
@@ -1385,7 +1408,9 @@ Item {
parentScreen: barWindow.screen
onToggleVpnPopup: {
vpnPopoutLoader.active = true
vpnPopoutLoader.item?.toggle()
if (vpnPopoutLoader.item) {
PopoutManager.requestPopout(vpnPopoutLoader.item, undefined, "vpn")
}
}
}
}
@@ -1422,7 +1447,7 @@ Item {
return
}
controlCenterLoader.item.triggerScreen = barWindow.screen
controlCenterLoader.item.toggle()
PopoutManager.requestPopout(controlCenterLoader.item, undefined, "controlCenter")
if (controlCenterLoader.item.shouldBeVisible && NetworkService.wifiEnabled) {
NetworkService.scanWifi()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.SystemTray
import Quickshell.Wayland
import Quickshell.Widgets
@@ -49,7 +50,17 @@ Item {
visible: allTrayItems.length > 0
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 {
id: visualBackground
@@ -171,7 +182,7 @@ Item {
if (!delegateRoot.trayItem.hasMenu) return
root.overflowWasOpenBeforeTrayMenu = root.menuOpen
root.menuOpen = false
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVertical, root.axis)
}
}
@@ -304,7 +315,7 @@ Item {
if (!delegateRoot.trayItem.hasMenu) return
root.overflowWasOpenBeforeTrayMenu = root.menuOpen
root.menuOpen = false
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVertical, root.axis)
}
}
@@ -356,10 +367,19 @@ Item {
screen: root.parentScreen
WlrLayershell.layer: WlrLayershell.Top
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"
color: "transparent"
HyprlandFocusGrab {
windows: [overflowMenu]
active: CompositorService.isHyprland && root.menuOpen
}
anchors {
top: true
left: true
@@ -372,10 +392,105 @@ Item {
: (screen?.devicePixelRatio || 1)
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: {
if (visible) {
if (currentTrayMenu) {
currentTrayMenu.showMenu = false
}
PopoutManager.closeAllPopouts()
ModalManager.closeAllModalsExcept(null)
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
root.overflowWasOpenBeforeTrayMenu = true
root.menuOpen = false
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 {
@@ -675,19 +783,9 @@ Item {
function close() {
showMenu = false
if (root.overflowWasOpenBeforeTrayMenu) {
root.menuOpen = true
Qt.callLater(() => {
if (overflowMenu.visible && overflowFocusScope) {
overflowFocusScope.forceActiveFocus()
}
})
}
root.overflowWasOpenBeforeTrayMenu = false
}
function closeWithAction() {
root.overflowWasOpenBeforeTrayMenu = false
close()
}
@@ -721,9 +819,18 @@ Item {
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
WlrLayershell.layer: WlrLayershell.Top
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"
HyprlandFocusGrab {
windows: [menuWindow]
active: CompositorService.isHyprland && menuRoot.showMenu
}
anchors {
top: true
left: true
@@ -736,10 +843,103 @@ Item {
: (screen?.devicePixelRatio || 1)
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: {
if (visible) {
root.menuOpen = false
PopoutManager.closeAllPopouts()
ModalManager.closeAllModalsExcept(null)
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
height: 16
anchors.verticalCenter: parent.verticalCenter
visible: menuEntry?.icon && menuEntry.icon !== ""
visible: (menuEntry?.icon ?? "") !== ""
Image {
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) {
if (parentWindow && typeof parentWindow.systemTrayMenuOpen !== "undefined") {
parentWindow.systemTrayMenuOpen = true
}
if (!screen) return
if (currentTrayMenu) {
currentTrayMenu.showMenu = false
currentTrayMenu.destroy()
currentTrayMenu = null
}
currentTrayMenu = trayMenuComponent.createObject(null)
if (currentTrayMenu) {
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj)
}
if (!currentTrayMenu) return
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj)
}
}

View File

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

View File

@@ -1086,9 +1086,9 @@ Item {
})
}
// 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
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"
return ["sh", "-c", findCmd]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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