1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-13 07:42:46 -04:00

Compare commits

..

59 Commits

Author SHA1 Message Date
bbedward 2ed6c33c83 missing import 2025-11-19 19:14:47 -05:00
bbedward 7ad532ed17 dankinstall: add ultramarine 2025-11-19 18:53:41 -05:00
bbedward 92fe8c5b14 hyprland: restore focus grab to tray menus 2025-11-19 17:24:14 -05:00
bbedward 8e95572589 modals: move HyprFocusGrab out of common Modal 2025-11-19 17:16:51 -05:00
bbedward 62da862a66 modal: round textureSize pixels 2025-11-19 14:36:08 -05:00
bbedward 993e34f548 dankinstall: weakdeps for niri/system 2025-11-19 09:35:22 -05:00
github-actions[bot] e39465aece chore: bump version to v0.6.2 2025-11-19 13:54:50 +00:00
bbedward 8fd616b680 osd: suppression fix from cc 2025-11-19 08:52:37 -05:00
bbedward cc054b27de filebrowser: fix auto closing from ddash 2025-11-19 08:33:07 -05:00
github-actions[bot] dfdaa82245 chore: bump version to v0.6.1 2025-11-19 03:16:35 +00:00
bbedward 99a307e0ad dankbar: hot fix color moda & systm tray item positions 2025-11-18 22:13:06 -05:00
github-actions[bot] 5ddea836a1 chore: bump version to v0.6.0 2025-11-18 23:52:39 +00:00
bbedward 208d92aa06 launcher: re-create grid on open 2025-11-18 18:50:42 -05:00
bbedward 6ef9ddd4f3 hyprland: fix right click overview 2025-11-18 17:53:00 -05:00
bbedward 1c92d39185 i18n: update translations 2025-11-18 17:21:45 -05:00
bbedward c0f072217c dankbar: split up monolithic file 2025-11-18 16:18:24 -05:00
bbedward 542562f988 dankbar: missing background click handler for plugin popout 2025-11-18 16:03:30 -05:00
bbedward 4e6f0d5e87 bluez: fix disappearing popouts with modal maanger 2025-11-18 14:36:10 -05:00
bbedward 10639a5ead re-add bound lost my qmlfmt 2025-11-17 20:53:55 -05:00
bbedward 06d668e710 launcher: new search algo
- replace fzf.js with custom levenshtein distance matching
- tweak scoring system
- more graceful fuzzy, more weight to prefixes
- basic tokenization
2025-11-17 20:52:04 -05:00
bbedward d1472dfcba osd: also have left center and right center options 2025-11-17 14:05:04 -05:00
bbedward ccb4da3cd8 extws: fix force option 2025-11-17 10:08:06 -05:00
bbedward 46e96b49f0 extws: fix capability check & don't show names 2025-11-17 09:50:06 -05:00
bbedward 984cfe7f98 labwc: use dms dpms off/on for idle service 2025-11-17 09:12:38 -05:00
bbedward d769300137 core/cli: add dpms off/on via wlr-output-power-management 2025-11-17 00:31:00 -05:00
Hikiru d175d66828 Add NixOS module (#734)
* default.nix: fix "wavelength" typo

* Add nixos module

typo

fix

* nix: refactor and fix nix modules

* nix: fix NixOS module import

* nix: revert quickshell option change

* nix: fix nixosModules dmsPkgs definition

---------

Co-authored-by: LuckShiba <luckshiba@protonmail.com>
2025-11-16 21:12:01 -05:00
bbedward c1a314332e wallpaper: rename blur layer option 2025-11-16 19:50:19 -05:00
bbedward 046ac59d21 core/extworkspace: only register outputs on name received 2025-11-16 19:40:46 -05:00
bbedward 00c06f07d0 workspace: fix ext-ws hiding 2025-11-16 18:52:12 -05:00
bbedward 3e2ab40c6a ws: 0 width when 0 workspaces, restore labwc to README 2025-11-16 17:53:50 -05:00
bbedward 350ffd0052 i18n: update terms 2025-11-16 16:33:55 -05:00
bbedward ecd1a622d2 display: fix wallpaper when using monitor model 2025-11-16 16:33:21 -05:00
bbedward f13968aa61 osd: configurable position 2025-11-16 16:27:01 -05:00
bbedward 4d1ffde54c launcher: allow launch prefix to run in shell 2025-11-16 16:14:19 -05:00
bbedward d69017a706 also update per-monitor wallpaper to accout for display setting 2025-11-16 16:01:11 -05:00
bbedward f2deaeccdb scaling: snap value reported by wlr-output 2025-11-16 15:56:59 -05:00
bbedward ea9b0d2a79 powermenu: use consistent new-style on locker + greeter
fixes #739
2025-11-16 15:05:06 -05:00
bbedward 2e6dbedb8b dwl/mango: support keyboard layout 2025-11-16 14:24:56 -05:00
bbedward 6f359df8f9 displays: allow filtering by model over name 2025-11-16 13:58:53 -05:00
claymorwan f6db20cd06 confirm-modal:add layer namespace (#743) 2025-11-16 13:09:44 -05:00
bbedward 6287fae065 running apps: don't wrap on scroll wheel
fixes #740
2025-11-16 13:06:40 -05:00
bbedward e441607ce3 colorpicker: don't include line break in copy
fixes #741
2025-11-16 13:00:13 -05:00
bbedward b5379a95fa qs/dankbar/meta: add a mask region to the bar
- Allows bar items to be clickable evn when popouts open
- Add state machines to manage state across monitors
- change focuses to ondemand on hyprland
2025-11-16 12:52:13 -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
178 changed files with 16539 additions and 4711 deletions
+6 -2
View File
@@ -19,7 +19,7 @@
</div> </div>
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop. DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
## Repository Structure ## Repository Structure
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## Supported Compositors ## Supported Compositors
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), and [MangoWC](https://github.com/DreamMaoMao/mangowc) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features. Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), and [labwc](https://labwc.github.io/) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors) [Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
@@ -183,6 +183,10 @@ For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeM
- [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration - [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration
- [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration - [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=AvengeMedia/DankMaterialShell&type=date&legend=top-left)](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
## License ## License
MIT License - See [LICENSE](LICENSE) for details. MIT License - See [LICENSE](LICENSE) for details.
+1
View File
@@ -368,6 +368,7 @@ func getCommonCommands() []*cobra.Command {
pluginsCmd, pluginsCmd,
dank16Cmd, dank16Cmd,
brightnessCmd, brightnessCmd,
dpmsCmd,
keybindsCmd, keybindsCmd,
greeterCmd, greeterCmd,
setupCmd, setupCmd,
+84
View File
@@ -0,0 +1,84 @@
package main
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var dpmsCmd = &cobra.Command{
Use: "dpms",
Short: "Control display power management",
}
var dpmsOnCmd = &cobra.Command{
Use: "on [output]",
Short: "Turn display(s) on",
Args: cobra.MaximumNArgs(1),
Run: runDPMSOn,
}
var dpmsOffCmd = &cobra.Command{
Use: "off [output]",
Short: "Turn display(s) off",
Args: cobra.MaximumNArgs(1),
Run: runDPMSOff,
}
var dpmsListCmd = &cobra.Command{
Use: "list",
Short: "List outputs",
Args: cobra.NoArgs,
Run: runDPMSList,
}
func init() {
dpmsCmd.AddCommand(dpmsOnCmd, dpmsOffCmd, dpmsListCmd)
}
func runDPMSOn(cmd *cobra.Command, args []string) {
outputName := ""
if len(args) > 0 {
outputName = args[0]
}
client, err := newDPMSClient()
if err != nil {
log.Fatalf("%v", err)
}
defer client.Close()
if err := client.SetDPMS(outputName, true); err != nil {
log.Fatalf("%v", err)
}
}
func runDPMSOff(cmd *cobra.Command, args []string) {
outputName := ""
if len(args) > 0 {
outputName = args[0]
}
client, err := newDPMSClient()
if err != nil {
log.Fatalf("%v", err)
}
defer client.Close()
if err := client.SetDPMS(outputName, false); err != nil {
log.Fatalf("%v", err)
}
}
func runDPMSList(cmd *cobra.Command, args []string) {
client, err := newDPMSClient()
if err != nil {
log.Fatalf("%v", err)
}
defer client.Close()
for _, output := range client.ListOutputs() {
fmt.Println(output)
}
}
+345
View File
@@ -0,0 +1,345 @@
package main
import (
"fmt"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_power"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type cmd struct {
fn func()
done chan error
}
type dpmsClient struct {
display *wlclient.Display
ctx *wlclient.Context
powerMgr *wlr_output_power.ZwlrOutputPowerManagerV1
outputs map[string]*outputState
mu sync.Mutex
syncRound int
done bool
err error
cmdq chan cmd
stopChan chan struct{}
wg sync.WaitGroup
}
type outputState struct {
wlOutput *wlclient.Output
powerCtrl *wlr_output_power.ZwlrOutputPowerV1
name string
mode uint32
failed bool
waitCh chan struct{}
wantMode *uint32
}
func (c *dpmsClient) post(fn func()) {
done := make(chan error, 1)
select {
case c.cmdq <- cmd{fn: fn, done: done}:
<-done
case <-c.stopChan:
}
}
func (c *dpmsClient) waylandActor() {
defer c.wg.Done()
for {
select {
case <-c.stopChan:
return
case cmd := <-c.cmdq:
cmd.fn()
close(cmd.done)
}
}
}
func newDPMSClient() (*dpmsClient, error) {
display, err := wlclient.Connect("")
if err != nil {
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
}
c := &dpmsClient{
display: display,
ctx: display.Context(),
outputs: make(map[string]*outputState),
cmdq: make(chan cmd, 128),
stopChan: make(chan struct{}),
}
c.wg.Add(1)
go c.waylandActor()
registry, err := display.GetRegistry()
if err != nil {
display.Context().Close()
return nil, fmt.Errorf("failed to get registry: %w", err)
}
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName:
powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx)
version := e.Version
if version > 1 {
version = 1
}
if err := registry.Bind(e.Name, e.Interface, version, powerMgr); err == nil {
c.powerMgr = powerMgr
}
case "wl_output":
output := wlclient.NewOutput(c.ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
outputID := fmt.Sprintf("output-%d", output.ID())
state := &outputState{
wlOutput: output,
name: outputID,
}
c.mu.Lock()
c.outputs[outputID] = state
c.mu.Unlock()
output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
c.mu.Lock()
delete(c.outputs, state.name)
state.name = ev.Name
c.outputs[ev.Name] = state
c.mu.Unlock()
})
}
}
})
syncCallback, err := display.Sync()
if err != nil {
c.Close()
return nil, fmt.Errorf("failed to sync display: %w", err)
}
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
c.handleSync()
})
for !c.done {
if err := c.ctx.Dispatch(); err != nil {
c.Close()
return nil, fmt.Errorf("dispatch error: %w", err)
}
}
if c.err != nil {
c.Close()
return nil, c.err
}
return c, nil
}
func (c *dpmsClient) handleSync() {
c.syncRound++
switch c.syncRound {
case 1:
if c.powerMgr == nil {
c.err = fmt.Errorf("wlr-output-power-management protocol not supported by compositor")
c.done = true
return
}
c.mu.Lock()
for _, state := range c.outputs {
powerCtrl, err := c.powerMgr.GetOutputPower(state.wlOutput)
if err != nil {
continue
}
state.powerCtrl = powerCtrl
powerCtrl.SetModeHandler(func(e wlr_output_power.ZwlrOutputPowerV1ModeEvent) {
c.mu.Lock()
defer c.mu.Unlock()
if state.powerCtrl == nil {
return
}
state.mode = e.Mode
if state.wantMode != nil && e.Mode == *state.wantMode && state.waitCh != nil {
close(state.waitCh)
state.wantMode = nil
}
})
powerCtrl.SetFailedHandler(func(e wlr_output_power.ZwlrOutputPowerV1FailedEvent) {
c.mu.Lock()
defer c.mu.Unlock()
if state.powerCtrl == nil {
return
}
state.failed = true
if state.waitCh != nil {
close(state.waitCh)
state.wantMode = nil
}
})
}
c.mu.Unlock()
syncCallback, err := c.display.Sync()
if err != nil {
c.err = fmt.Errorf("failed to sync display: %w", err)
c.done = true
return
}
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
c.handleSync()
})
default:
c.done = true
}
}
func (c *dpmsClient) ListOutputs() []string {
c.mu.Lock()
defer c.mu.Unlock()
names := make([]string, 0, len(c.outputs))
for name := range c.outputs {
names = append(names, name)
}
return names
}
func (c *dpmsClient) SetDPMS(outputName string, on bool) error {
var mode uint32
if on {
mode = uint32(wlr_output_power.ZwlrOutputPowerV1ModeOn)
} else {
mode = uint32(wlr_output_power.ZwlrOutputPowerV1ModeOff)
}
var setErr error
c.post(func() {
c.mu.Lock()
var waitStates []*outputState
if outputName == "" || outputName == "all" {
if len(c.outputs) == 0 {
c.mu.Unlock()
setErr = fmt.Errorf("no outputs found")
return
}
for _, state := range c.outputs {
if state.powerCtrl == nil {
continue
}
state.wantMode = &mode
state.waitCh = make(chan struct{})
state.failed = false
waitStates = append(waitStates, state)
state.powerCtrl.SetMode(mode)
}
} else {
state, ok := c.outputs[outputName]
if !ok {
c.mu.Unlock()
setErr = fmt.Errorf("output not found: %s", outputName)
return
}
if state.powerCtrl == nil {
c.mu.Unlock()
setErr = fmt.Errorf("output %s has nil powerCtrl", outputName)
return
}
state.wantMode = &mode
state.waitCh = make(chan struct{})
state.failed = false
waitStates = append(waitStates, state)
state.powerCtrl.SetMode(mode)
}
c.mu.Unlock()
deadline := time.Now().Add(10 * time.Second)
for _, state := range waitStates {
c.mu.Lock()
ch := state.waitCh
c.mu.Unlock()
done := false
for !done {
if err := c.ctx.Dispatch(); err != nil {
setErr = fmt.Errorf("dispatch error: %w", err)
return
}
select {
case <-ch:
c.mu.Lock()
if state.failed {
setErr = fmt.Errorf("compositor reported failed for %s", state.name)
c.mu.Unlock()
return
}
c.mu.Unlock()
done = true
default:
if time.Now().After(deadline) {
setErr = fmt.Errorf("timeout waiting for mode change on %s", state.name)
return
}
time.Sleep(10 * time.Millisecond)
}
}
}
c.mu.Lock()
for _, state := range waitStates {
if state.powerCtrl != nil {
state.powerCtrl.Destroy()
state.powerCtrl = nil
}
}
c.mu.Unlock()
c.display.Roundtrip()
})
return setErr
}
func (c *dpmsClient) Close() {
close(c.stopChan)
c.wg.Wait()
c.mu.Lock()
defer c.mu.Unlock()
for _, state := range c.outputs {
if state.powerCtrl != nil {
state.powerCtrl.Destroy()
}
}
c.outputs = nil
if c.powerMgr != nil {
c.powerMgr.Destroy()
c.powerMgr = nil
}
if c.display != nil {
c.ctx.Close()
c.display = nil
}
}
-1
View File
@@ -13,7 +13,6 @@ require (
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
github.com/spf13/cobra v1.10.1 github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
) )
-2
View File
@@ -125,8 +125,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34 h1:iTAt1me6SBYsuzrl/CmrxtATPlOG/pVviosM3DhUdKE=
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34/go.mod h1:jzmUN5lUAv2O8e63OvcauV4S30rIZ1BvF/PNYE37vDo=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
+11 -1
View File
@@ -19,10 +19,12 @@ func init() {
Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan) return NewFedoraDistribution(config, logChan)
}) })
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan) return NewFedoraDistribution(config, logChan)
}) })
Register("ultramarine", "#00078b", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
} }
type FedoraDistribution struct { type FedoraDistribution struct {
@@ -506,6 +508,14 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", "))) f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
args := []string{"dnf", "install", "-y"} args := []string{"dnf", "install", "-y"}
for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
}
}
args = append(args, packages...) args = append(args, packages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
+302 -2
View File
@@ -1,12 +1,12 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/dwl-ipc-unstable-v2.xml // XML file : internal/proto/xml/dwl-ipc-unstable-v2.xml
// //
// dwl_ipc_unstable_v2 Protocol Copyright: // dwl_ipc_unstable_v2 Protocol Copyright:
package dwl_ipc package dwl_ipc
import "github.com/yaslama/go-wayland/wayland/client" import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
// ZdwlIpcManagerV2InterfaceName is the name of the interface as it appears in the [client.Registry]. // ZdwlIpcManagerV2InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the // It can be used to match the [client.RegistryGlobalEvent.Interface] in the
@@ -157,6 +157,16 @@ type ZdwlIpcOutputV2 struct {
appidHandler ZdwlIpcOutputV2AppidHandlerFunc appidHandler ZdwlIpcOutputV2AppidHandlerFunc
layoutSymbolHandler ZdwlIpcOutputV2LayoutSymbolHandlerFunc layoutSymbolHandler ZdwlIpcOutputV2LayoutSymbolHandlerFunc
frameHandler ZdwlIpcOutputV2FrameHandlerFunc frameHandler ZdwlIpcOutputV2FrameHandlerFunc
fullscreenHandler ZdwlIpcOutputV2FullscreenHandlerFunc
floatingHandler ZdwlIpcOutputV2FloatingHandlerFunc
xHandler ZdwlIpcOutputV2XHandlerFunc
yHandler ZdwlIpcOutputV2YHandlerFunc
widthHandler ZdwlIpcOutputV2WidthHandlerFunc
heightHandler ZdwlIpcOutputV2HeightHandlerFunc
lastLayerHandler ZdwlIpcOutputV2LastLayerHandlerFunc
kbLayoutHandler ZdwlIpcOutputV2KbLayoutHandlerFunc
keymodeHandler ZdwlIpcOutputV2KeymodeHandlerFunc
scalefactorHandler ZdwlIpcOutputV2ScalefactorHandlerFunc
} }
// NewZdwlIpcOutputV2 : control dwl output // NewZdwlIpcOutputV2 : control dwl output
@@ -251,6 +261,60 @@ func (i *ZdwlIpcOutputV2) SetLayout(index uint32) error {
return err return err
} }
// Quit : Quit mango
// This request allows clients to instruct the compositor to quit mango.
func (i *ZdwlIpcOutputV2) Quit() error {
const opcode = 4
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SendDispatch : Set the active tags of this output
//
// dispatch: dispatch name.
// arg1: arg1.
// arg2: arg2.
// arg3: arg3.
// arg4: arg4.
// arg5: arg5.
func (i *ZdwlIpcOutputV2) SendDispatch(dispatch, arg1, arg2, arg3, arg4, arg5 string) error {
const opcode = 5
dispatchLen := client.PaddedLen(len(dispatch) + 1)
arg1Len := client.PaddedLen(len(arg1) + 1)
arg2Len := client.PaddedLen(len(arg2) + 1)
arg3Len := client.PaddedLen(len(arg3) + 1)
arg4Len := client.PaddedLen(len(arg4) + 1)
arg5Len := client.PaddedLen(len(arg5) + 1)
_reqBufLen := 8 + (4 + dispatchLen) + (4 + arg1Len) + (4 + arg2Len) + (4 + arg3Len) + (4 + arg4Len) + (4 + arg5Len)
_reqBuf := make([]byte, _reqBufLen)
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutString(_reqBuf[l:l+(4+dispatchLen)], dispatch)
l += (4 + dispatchLen)
client.PutString(_reqBuf[l:l+(4+arg1Len)], arg1)
l += (4 + arg1Len)
client.PutString(_reqBuf[l:l+(4+arg2Len)], arg2)
l += (4 + arg2Len)
client.PutString(_reqBuf[l:l+(4+arg3Len)], arg3)
l += (4 + arg3Len)
client.PutString(_reqBuf[l:l+(4+arg4Len)], arg4)
l += (4 + arg4Len)
client.PutString(_reqBuf[l:l+(4+arg5Len)], arg5)
l += (4 + arg5Len)
err := i.Context().WriteMsg(_reqBuf, nil)
return err
}
type ZdwlIpcOutputV2TagState uint32 type ZdwlIpcOutputV2TagState uint32
// ZdwlIpcOutputV2TagState : // ZdwlIpcOutputV2TagState :
@@ -399,6 +463,136 @@ func (i *ZdwlIpcOutputV2) SetFrameHandler(f ZdwlIpcOutputV2FrameHandlerFunc) {
i.frameHandler = f i.frameHandler = f
} }
// ZdwlIpcOutputV2FullscreenEvent : Update fullscreen status
//
// Indicates if the selected client on this output is fullscreen.
type ZdwlIpcOutputV2FullscreenEvent struct {
IsFullscreen uint32
}
type ZdwlIpcOutputV2FullscreenHandlerFunc func(ZdwlIpcOutputV2FullscreenEvent)
// SetFullscreenHandler : sets handler for ZdwlIpcOutputV2FullscreenEvent
func (i *ZdwlIpcOutputV2) SetFullscreenHandler(f ZdwlIpcOutputV2FullscreenHandlerFunc) {
i.fullscreenHandler = f
}
// ZdwlIpcOutputV2FloatingEvent : Update the floating status
//
// Indicates if the selected client on this output is floating.
type ZdwlIpcOutputV2FloatingEvent struct {
IsFloating uint32
}
type ZdwlIpcOutputV2FloatingHandlerFunc func(ZdwlIpcOutputV2FloatingEvent)
// SetFloatingHandler : sets handler for ZdwlIpcOutputV2FloatingEvent
func (i *ZdwlIpcOutputV2) SetFloatingHandler(f ZdwlIpcOutputV2FloatingHandlerFunc) {
i.floatingHandler = f
}
// ZdwlIpcOutputV2XEvent : Update the x coordinates
//
// Indicates if x coordinates of the selected client.
type ZdwlIpcOutputV2XEvent struct {
X int32
}
type ZdwlIpcOutputV2XHandlerFunc func(ZdwlIpcOutputV2XEvent)
// SetXHandler : sets handler for ZdwlIpcOutputV2XEvent
func (i *ZdwlIpcOutputV2) SetXHandler(f ZdwlIpcOutputV2XHandlerFunc) {
i.xHandler = f
}
// ZdwlIpcOutputV2YEvent : Update the y coordinates
//
// Indicates if y coordinates of the selected client.
type ZdwlIpcOutputV2YEvent struct {
Y int32
}
type ZdwlIpcOutputV2YHandlerFunc func(ZdwlIpcOutputV2YEvent)
// SetYHandler : sets handler for ZdwlIpcOutputV2YEvent
func (i *ZdwlIpcOutputV2) SetYHandler(f ZdwlIpcOutputV2YHandlerFunc) {
i.yHandler = f
}
// ZdwlIpcOutputV2WidthEvent : Update the width
//
// Indicates if width of the selected client.
type ZdwlIpcOutputV2WidthEvent struct {
Width int32
}
type ZdwlIpcOutputV2WidthHandlerFunc func(ZdwlIpcOutputV2WidthEvent)
// SetWidthHandler : sets handler for ZdwlIpcOutputV2WidthEvent
func (i *ZdwlIpcOutputV2) SetWidthHandler(f ZdwlIpcOutputV2WidthHandlerFunc) {
i.widthHandler = f
}
// ZdwlIpcOutputV2HeightEvent : Update the height
//
// Indicates if height of the selected client.
type ZdwlIpcOutputV2HeightEvent struct {
Height int32
}
type ZdwlIpcOutputV2HeightHandlerFunc func(ZdwlIpcOutputV2HeightEvent)
// SetHeightHandler : sets handler for ZdwlIpcOutputV2HeightEvent
func (i *ZdwlIpcOutputV2) SetHeightHandler(f ZdwlIpcOutputV2HeightHandlerFunc) {
i.heightHandler = f
}
// ZdwlIpcOutputV2LastLayerEvent : last map layer.
//
// last map layer.
type ZdwlIpcOutputV2LastLayerEvent struct {
LastLayer string
}
type ZdwlIpcOutputV2LastLayerHandlerFunc func(ZdwlIpcOutputV2LastLayerEvent)
// SetLastLayerHandler : sets handler for ZdwlIpcOutputV2LastLayerEvent
func (i *ZdwlIpcOutputV2) SetLastLayerHandler(f ZdwlIpcOutputV2LastLayerHandlerFunc) {
i.lastLayerHandler = f
}
// ZdwlIpcOutputV2KbLayoutEvent : current keyboard layout.
//
// current keyboard layout.
type ZdwlIpcOutputV2KbLayoutEvent struct {
KbLayout string
}
type ZdwlIpcOutputV2KbLayoutHandlerFunc func(ZdwlIpcOutputV2KbLayoutEvent)
// SetKbLayoutHandler : sets handler for ZdwlIpcOutputV2KbLayoutEvent
func (i *ZdwlIpcOutputV2) SetKbLayoutHandler(f ZdwlIpcOutputV2KbLayoutHandlerFunc) {
i.kbLayoutHandler = f
}
// ZdwlIpcOutputV2KeymodeEvent : current keybind mode.
//
// current keybind mode.
type ZdwlIpcOutputV2KeymodeEvent struct {
Keymode string
}
type ZdwlIpcOutputV2KeymodeHandlerFunc func(ZdwlIpcOutputV2KeymodeEvent)
// SetKeymodeHandler : sets handler for ZdwlIpcOutputV2KeymodeEvent
func (i *ZdwlIpcOutputV2) SetKeymodeHandler(f ZdwlIpcOutputV2KeymodeHandlerFunc) {
i.keymodeHandler = f
}
// ZdwlIpcOutputV2ScalefactorEvent : scale factor of monitor.
//
// scale factor of monitor.
type ZdwlIpcOutputV2ScalefactorEvent struct {
Scalefactor uint32
}
type ZdwlIpcOutputV2ScalefactorHandlerFunc func(ZdwlIpcOutputV2ScalefactorEvent)
// SetScalefactorHandler : sets handler for ZdwlIpcOutputV2ScalefactorEvent
func (i *ZdwlIpcOutputV2) SetScalefactorHandler(f ZdwlIpcOutputV2ScalefactorHandlerFunc) {
i.scalefactorHandler = f
}
func (i *ZdwlIpcOutputV2) Dispatch(opcode uint32, fd int, data []byte) { func (i *ZdwlIpcOutputV2) Dispatch(opcode uint32, fd int, data []byte) {
switch opcode { switch opcode {
case 0: case 0:
@@ -487,5 +681,111 @@ func (i *ZdwlIpcOutputV2) Dispatch(opcode uint32, fd int, data []byte) {
var e ZdwlIpcOutputV2FrameEvent var e ZdwlIpcOutputV2FrameEvent
i.frameHandler(e) i.frameHandler(e)
case 8:
if i.fullscreenHandler == nil {
return
}
var e ZdwlIpcOutputV2FullscreenEvent
l := 0
e.IsFullscreen = client.Uint32(data[l : l+4])
l += 4
i.fullscreenHandler(e)
case 9:
if i.floatingHandler == nil {
return
}
var e ZdwlIpcOutputV2FloatingEvent
l := 0
e.IsFloating = client.Uint32(data[l : l+4])
l += 4
i.floatingHandler(e)
case 10:
if i.xHandler == nil {
return
}
var e ZdwlIpcOutputV2XEvent
l := 0
e.X = int32(client.Uint32(data[l : l+4]))
l += 4
i.xHandler(e)
case 11:
if i.yHandler == nil {
return
}
var e ZdwlIpcOutputV2YEvent
l := 0
e.Y = int32(client.Uint32(data[l : l+4]))
l += 4
i.yHandler(e)
case 12:
if i.widthHandler == nil {
return
}
var e ZdwlIpcOutputV2WidthEvent
l := 0
e.Width = int32(client.Uint32(data[l : l+4]))
l += 4
i.widthHandler(e)
case 13:
if i.heightHandler == nil {
return
}
var e ZdwlIpcOutputV2HeightEvent
l := 0
e.Height = int32(client.Uint32(data[l : l+4]))
l += 4
i.heightHandler(e)
case 14:
if i.lastLayerHandler == nil {
return
}
var e ZdwlIpcOutputV2LastLayerEvent
l := 0
lastLayerLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.LastLayer = client.String(data[l : l+lastLayerLen])
l += lastLayerLen
i.lastLayerHandler(e)
case 15:
if i.kbLayoutHandler == nil {
return
}
var e ZdwlIpcOutputV2KbLayoutEvent
l := 0
kbLayoutLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.KbLayout = client.String(data[l : l+kbLayoutLen])
l += kbLayoutLen
i.kbLayoutHandler(e)
case 16:
if i.keymodeHandler == nil {
return
}
var e ZdwlIpcOutputV2KeymodeEvent
l := 0
keymodeLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.Keymode = client.String(data[l : l+keymodeLen])
l += keymodeLen
i.keymodeHandler(e)
case 17:
if i.scalefactorHandler == nil {
return
}
var e ZdwlIpcOutputV2ScalefactorEvent
l := 0
e.Scalefactor = client.Uint32(data[l : l+4])
l += 4
i.scalefactorHandler(e)
} }
} }
@@ -1,5 +1,5 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : ext-workspace-v1.xml // XML file : ext-workspace-v1.xml
// //
// ext_workspace_v1 Protocol Copyright: // ext_workspace_v1 Protocol Copyright:
@@ -35,7 +35,8 @@ import (
"reflect" "reflect"
"unsafe" "unsafe"
"github.com/yaslama/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
// registerServerProxy registers a proxy with a server-assigned ID. // registerServerProxy registers a proxy with a server-assigned ID.
@@ -61,8 +62,9 @@ func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint3
return return
} }
objectsMap := reflect.NewAt(objectsField.Type(), unsafe.Pointer(objectsField.UnsafeAddr())).Elem() objectsMapPtr := unsafe.Pointer(objectsField.UnsafeAddr())
objectsMap.SetMapIndex(reflect.ValueOf(serverID), reflect.ValueOf(proxy)) objectsMap := (*syncmap.Map[uint32, client.Proxy])(objectsMapPtr)
objectsMap.Store(serverID, proxy)
} }
// ExtWorkspaceManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry]. // ExtWorkspaceManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
@@ -1,5 +1,5 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : wayland-protocols/wlr-gamma-control-unstable-v1.xml // XML file : wayland-protocols/wlr-gamma-control-unstable-v1.xml
// //
// wlr_gamma_control_unstable_v1 Protocol Copyright: // wlr_gamma_control_unstable_v1 Protocol Copyright:
@@ -31,7 +31,7 @@
package wlr_gamma_control package wlr_gamma_control
import ( import (
"github.com/yaslama/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@@ -1,5 +1,5 @@
// Generated by go-wayland-scanner // Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner // https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : /home/brandon/repos/dankdots/wlr-output-management-unstable-v1.xml // XML file : /home/brandon/repos/dankdots/wlr-output-management-unstable-v1.xml
// //
// wlr_output_management_unstable_v1 Protocol Copyright: // wlr_output_management_unstable_v1 Protocol Copyright:
@@ -33,7 +33,8 @@ import (
"reflect" "reflect"
"unsafe" "unsafe"
"github.com/yaslama/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint32) { func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint32) {
@@ -47,9 +48,9 @@ func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint3
if !objectsField.IsValid() { if !objectsField.IsValid() {
return return
} }
objectsField = reflect.NewAt(objectsField.Type(), unsafe.Pointer(objectsField.UnsafeAddr())).Elem() objectsMapPtr := unsafe.Pointer(objectsField.UnsafeAddr())
objectsMap := objectsField.Interface().(map[uint32]client.Proxy) objectsMap := (*syncmap.Map[uint32, client.Proxy])(objectsMapPtr)
objectsMap[serverID] = proxy objectsMap.Store(serverID, proxy)
} }
// ZwlrOutputManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry]. // ZwlrOutputManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
@@ -0,0 +1,283 @@
// Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/wlr-output-power-management-unstable-v1.xml
//
// wlr_output_power_management_unstable_v1 Protocol Copyright:
//
// Copyright © 2019 Purism SPC
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice (including the next
// paragraph) shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
package wlr_output_power
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
// ZwlrOutputPowerManagerV1InterfaceName 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
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwlrOutputPowerManagerV1InterfaceName = "zwlr_output_power_manager_v1"
// ZwlrOutputPowerManagerV1 : manager to create per-output power management
//
// This interface is a manager that allows creating per-output power
// management mode controls.
type ZwlrOutputPowerManagerV1 struct {
client.BaseProxy
}
// NewZwlrOutputPowerManagerV1 : manager to create per-output power management
//
// This interface is a manager that allows creating per-output power
// management mode controls.
func NewZwlrOutputPowerManagerV1(ctx *client.Context) *ZwlrOutputPowerManagerV1 {
zwlrOutputPowerManagerV1 := &ZwlrOutputPowerManagerV1{}
ctx.Register(zwlrOutputPowerManagerV1)
return zwlrOutputPowerManagerV1
}
// GetOutputPower : get a power management for an output
//
// Create an output power management mode control that can be used to
// adjust the power management mode for a given output.
func (i *ZwlrOutputPowerManagerV1) GetOutputPower(output *client.Output) (*ZwlrOutputPowerV1, error) {
id := NewZwlrOutputPowerV1(i.Context())
const opcode = 0
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], id.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], output.ID())
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return id, err
}
// Destroy : destroy the manager
//
// All objects created by the manager will still remain valid, until their
// appropriate destroy request has been called.
func (i *ZwlrOutputPowerManagerV1) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 1
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// ZwlrOutputPowerV1InterfaceName 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
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwlrOutputPowerV1InterfaceName = "zwlr_output_power_v1"
// ZwlrOutputPowerV1 : adjust power management mode for an output
//
// This object offers requests to set the power management mode of
// an output.
type ZwlrOutputPowerV1 struct {
client.BaseProxy
modeHandler ZwlrOutputPowerV1ModeHandlerFunc
failedHandler ZwlrOutputPowerV1FailedHandlerFunc
}
// NewZwlrOutputPowerV1 : adjust power management mode for an output
//
// This object offers requests to set the power management mode of
// an output.
func NewZwlrOutputPowerV1(ctx *client.Context) *ZwlrOutputPowerV1 {
zwlrOutputPowerV1 := &ZwlrOutputPowerV1{}
ctx.Register(zwlrOutputPowerV1)
return zwlrOutputPowerV1
}
// SetMode : Set an outputs power save mode
//
// Set an output's power save mode to the given mode. The mode change
// is effective immediately. If the output does not support the given
// mode a failed event is sent.
//
// mode: the power save mode to set
func (i *ZwlrOutputPowerV1) SetMode(mode uint32) error {
const opcode = 0
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(mode))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// Destroy : destroy this power management
//
// Destroys the output power management mode control object.
func (i *ZwlrOutputPowerV1) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 1
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
type ZwlrOutputPowerV1Mode uint32
// ZwlrOutputPowerV1Mode :
const (
// ZwlrOutputPowerV1ModeOff : Output is turned off.
ZwlrOutputPowerV1ModeOff ZwlrOutputPowerV1Mode = 0
// ZwlrOutputPowerV1ModeOn : Output is turned on, no power saving
ZwlrOutputPowerV1ModeOn ZwlrOutputPowerV1Mode = 1
)
func (e ZwlrOutputPowerV1Mode) Name() string {
switch e {
case ZwlrOutputPowerV1ModeOff:
return "off"
case ZwlrOutputPowerV1ModeOn:
return "on"
default:
return ""
}
}
func (e ZwlrOutputPowerV1Mode) Value() string {
switch e {
case ZwlrOutputPowerV1ModeOff:
return "0"
case ZwlrOutputPowerV1ModeOn:
return "1"
default:
return ""
}
}
func (e ZwlrOutputPowerV1Mode) String() string {
return e.Name() + "=" + e.Value()
}
type ZwlrOutputPowerV1Error uint32
// ZwlrOutputPowerV1Error :
const (
// ZwlrOutputPowerV1ErrorInvalidMode : nonexistent power save mode
ZwlrOutputPowerV1ErrorInvalidMode ZwlrOutputPowerV1Error = 1
)
func (e ZwlrOutputPowerV1Error) Name() string {
switch e {
case ZwlrOutputPowerV1ErrorInvalidMode:
return "invalid_mode"
default:
return ""
}
}
func (e ZwlrOutputPowerV1Error) Value() string {
switch e {
case ZwlrOutputPowerV1ErrorInvalidMode:
return "1"
default:
return ""
}
}
func (e ZwlrOutputPowerV1Error) String() string {
return e.Name() + "=" + e.Value()
}
// ZwlrOutputPowerV1ModeEvent : Report a power management mode change
//
// Report the power management mode change of an output.
//
// The mode event is sent after an output changed its power
// management mode. The reason can be a client using set_mode or the
// compositor deciding to change an output's mode.
// This event is also sent immediately when the object is created
// so the client is informed about the current power management mode.
type ZwlrOutputPowerV1ModeEvent struct {
Mode uint32
}
type ZwlrOutputPowerV1ModeHandlerFunc func(ZwlrOutputPowerV1ModeEvent)
// SetModeHandler : sets handler for ZwlrOutputPowerV1ModeEvent
func (i *ZwlrOutputPowerV1) SetModeHandler(f ZwlrOutputPowerV1ModeHandlerFunc) {
i.modeHandler = f
}
// ZwlrOutputPowerV1FailedEvent : object no longer valid
//
// This event indicates that the output power management mode control
// is no longer valid. This can happen for a number of reasons,
// including:
// - The output doesn't support power management
// - Another client already has exclusive power management mode control
// for this output
// - The output disappeared
//
// Upon receiving this event, the client should destroy this object.
type ZwlrOutputPowerV1FailedEvent struct{}
type ZwlrOutputPowerV1FailedHandlerFunc func(ZwlrOutputPowerV1FailedEvent)
// SetFailedHandler : sets handler for ZwlrOutputPowerV1FailedEvent
func (i *ZwlrOutputPowerV1) SetFailedHandler(f ZwlrOutputPowerV1FailedHandlerFunc) {
i.failedHandler = f
}
func (i *ZwlrOutputPowerV1) Dispatch(opcode uint32, fd int, data []byte) {
switch opcode {
case 0:
if i.modeHandler == nil {
return
}
var e ZwlrOutputPowerV1ModeEvent
l := 0
e.Mode = client.Uint32(data[l : l+4])
l += 4
i.modeHandler(e)
case 1:
if i.failedHandler == nil {
return
}
var e ZwlrOutputPowerV1FailedEvent
i.failedHandler(e)
}
}
@@ -19,7 +19,7 @@ I would probably just submit raphi's patchset but I don't think that would be po
reset. reset.
</description> </description>
<interface name="zdwl_ipc_manager_v2" version="1"> <interface name="zdwl_ipc_manager_v2" version="2">
<description summary="manage dwl state"> <description summary="manage dwl state">
This interface is exposed as a global in wl_registry. This interface is exposed as a global in wl_registry.
@@ -60,7 +60,7 @@ I would probably just submit raphi's patchset but I don't think that would be po
</event> </event>
</interface> </interface>
<interface name="zdwl_ipc_output_v2" version="1"> <interface name="zdwl_ipc_output_v2" version="2">
<description summary="control dwl output"> <description summary="control dwl output">
Observe and control a dwl output. Observe and control a dwl output.
@@ -162,5 +162,91 @@ I would probably just submit raphi's patchset but I don't think that would be po
<description summary="Set the layout of this output"/> <description summary="Set the layout of this output"/>
<arg name="index" type="uint" summary="index of a layout recieved by dwl_ipc_manager.layout"/> <arg name="index" type="uint" summary="index of a layout recieved by dwl_ipc_manager.layout"/>
</request> </request>
<request name="quit" since="2">
<description summary="Quit mango">This request allows clients to instruct the compositor to quit mango.</description>
</request>
<request name="dispatch" since="2">
<description summary="Set the active tags of this output"/>
<arg name="dispatch" type="string" summary="dispatch name."/>
<arg name="arg1" type="string" summary="arg1."/>
<arg name="arg2" type="string" summary="arg2."/>
<arg name="arg3" type="string" summary="arg3."/>
<arg name="arg4" type="string" summary="arg4."/>
<arg name="arg5" type="string" summary="arg5."/>
</request>
<!-- Version 2 -->
<event name="fullscreen" since="2">
<description summary="Update fullscreen status">
Indicates if the selected client on this output is fullscreen.
</description>
<arg name="is_fullscreen" type="uint" summary="If the selected client is fullscreen. Nonzero is valid, zero invalid"/>
</event>
<event name="floating" since="2">
<description summary="Update the floating status">
Indicates if the selected client on this output is floating.
</description>
<arg name="is_floating" type="uint" summary="If the selected client is floating. Nonzero is valid, zero invalid"/>
</event>
<event name="x" since="2">
<description summary="Update the x coordinates">
Indicates if x coordinates of the selected client.
</description>
<arg name="x" type="int" summary="x coordinate of the selected client"/>
</event>
<event name="y" since="2">
<description summary="Update the y coordinates">
Indicates if y coordinates of the selected client.
</description>
<arg name="y" type="int" summary="y coordinate of the selected client"/>
</event>
<event name="width" since="2">
<description summary="Update the width">
Indicates if width of the selected client.
</description>
<arg name="width" type="int" summary="width of the selected client"/>
</event>
<event name="height" since="2">
<description summary="Update the height">
Indicates if height of the selected client.
</description>
<arg name="height" type="int" summary="height of the selected client"/>
</event>
<event name="last_layer" since="2">
<description summary="last map layer.">
last map layer.
</description>
<arg name="last_layer" type="string" summary="last map layer."/>
</event>
<event name="kb_layout" since="2">
<description summary="current keyboard layout.">
current keyboard layout.
</description>
<arg name="kb_layout" type="string" summary="current keyboard layout."/>
</event>
<event name="keymode" since="2">
<description summary="current keybind mode.">
current keybind mode.
</description>
<arg name="keymode" type="string" summary="current keybind mode."/>
</event>
<event name="scalefactor" since="2">
<description summary="scale factor of monitor.">
scale factor of monitor.
</description>
<arg name="scalefactor" type="uint" summary="scale factor of monitor."/>
</event>
</interface> </interface>
</protocol> </protocol>
@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_output_power_management_unstable_v1">
<copyright>
Copyright © 2019 Purism SPC
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</copyright>
<description summary="Control power management modes of outputs">
This protocol allows clients to control power management modes
of outputs that are currently part of the compositor space. The
intent is to allow special clients like desktop shells to power
down outputs when the system is idle.
To modify outputs not currently part of the compositor space see
wlr-output-management.
Warning! The protocol described in this file is experimental and
backward incompatible changes may be made. Backward compatible changes
may be added together with the corresponding interface version bump.
Backward incompatible changes are done by bumping the version number in
the protocol and interface names and resetting the interface version.
Once the protocol is to be declared stable, the 'z' prefix and the
version number in the protocol and interface names are removed and the
interface version number is reset.
</description>
<interface name="zwlr_output_power_manager_v1" version="1">
<description summary="manager to create per-output power management">
This interface is a manager that allows creating per-output power
management mode controls.
</description>
<request name="get_output_power">
<description summary="get a power management for an output">
Create an output power management mode control that can be used to
adjust the power management mode for a given output.
</description>
<arg name="id" type="new_id" interface="zwlr_output_power_v1"/>
<arg name="output" type="object" interface="wl_output"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the manager">
All objects created by the manager will still remain valid, until their
appropriate destroy request has been called.
</description>
</request>
</interface>
<interface name="zwlr_output_power_v1" version="1">
<description summary="adjust power management mode for an output">
This object offers requests to set the power management mode of
an output.
</description>
<enum name="mode">
<entry name="off" value="0"
summary="Output is turned off."/>
<entry name="on" value="1"
summary="Output is turned on, no power saving"/>
</enum>
<enum name="error">
<entry name="invalid_mode" value="1" summary="nonexistent power save mode"/>
</enum>
<request name="set_mode">
<description summary="Set an outputs power save mode">
Set an output's power save mode to the given mode. The mode change
is effective immediately. If the output does not support the given
mode a failed event is sent.
</description>
<arg name="mode" type="uint" enum="mode" summary="the power save mode to set"/>
</request>
<event name="mode">
<description summary="Report a power management mode change">
Report the power management mode change of an output.
The mode event is sent after an output changed its power
management mode. The reason can be a client using set_mode or the
compositor deciding to change an output's mode.
This event is also sent immediately when the object is created
so the client is informed about the current power management mode.
</description>
<arg name="mode" type="uint" enum="mode"
summary="the output's new power management mode"/>
</event>
<event name="failed">
<description summary="object no longer valid">
This event indicates that the output power management mode control
is no longer valid. This can happen for a number of reasons,
including:
- The output doesn't support power management
- Another client already has exclusive power management mode control
for this output
- The output disappeared
Upon receiving this event, the client should destroy this object.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="destroy this power management">
Destroys the output power management mode control object.
</description>
</request>
</interface>
</protocol>
+4 -4
View File
@@ -165,12 +165,11 @@ func (a *BluezAgent) DisplayPasskey(device dbus.ObjectPath, passkey uint32, ente
log.Infof("[BluezAgent] DisplayPasskey: device=%s, passkey=%06d, entered=%d", device, passkey, entered) log.Infof("[BluezAgent] DisplayPasskey: device=%s, passkey=%06d, entered=%d", device, passkey, entered)
if entered == 0 { if entered == 0 {
pk := passkey passkeyStr := strconv.FormatUint(uint64(passkey), 10)
_, err := a.promptFor(device, "display-passkey", []string{}, nil) _, err := a.promptFor(device, "display-passkey", []string{}, &passkeyStr)
if err != nil { if err != nil {
log.Warnf("[BluezAgent] DisplayPasskey acknowledgment failed: %v", err) log.Warnf("[BluezAgent] DisplayPasskey acknowledgment failed: %v", err)
} }
_ = pk
} }
return nil return nil
@@ -179,7 +178,8 @@ func (a *BluezAgent) DisplayPasskey(device dbus.ObjectPath, passkey uint32, ente
func (a *BluezAgent) RequestConfirmation(device dbus.ObjectPath, passkey uint32) *dbus.Error { func (a *BluezAgent) RequestConfirmation(device dbus.ObjectPath, passkey uint32) *dbus.Error {
log.Infof("[BluezAgent] RequestConfirmation: device=%s, passkey=%06d", device, passkey) log.Infof("[BluezAgent] RequestConfirmation: device=%s, passkey=%06d", device, passkey)
secrets, err := a.promptFor(device, "confirm", []string{"decision"}, nil) passkeyStr := strconv.FormatUint(uint64(passkey), 10)
secrets, err := a.promptFor(device, "confirm", []string{"decision"}, &passkeyStr)
if err != nil { if err != nil {
log.Warnf("[BluezAgent] RequestConfirmation failed: %v", err) log.Warnf("[BluezAgent] RequestConfirmation failed: %v", err)
return a.errorFrom(err) return a.errorFrom(err)
+43 -74
View File
@@ -30,17 +30,13 @@ func NewManager() (*Manager, error) {
PairedDevices: []Device{}, PairedDevices: []Device{},
ConnectedDevices: []Device{}, ConnectedDevices: []Device{},
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan BluetoothState),
subMutex: sync.RWMutex{}, stopChan: make(chan struct{}),
stopChan: make(chan struct{}), dbusConn: conn,
dbusConn: conn, signals: make(chan *dbus.Signal, 256),
signals: make(chan *dbus.Signal, 256), dirty: make(chan struct{}, 1),
pairingSubscribers: make(map[string]chan PairingPrompt), eventQueue: make(chan func(), 32),
pairingSubMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
pendingPairings: make(map[string]bool),
eventQueue: make(chan func(), 32),
} }
broker := NewSubscriptionBroker(m.broadcastPairingPrompt) broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
@@ -358,26 +354,25 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
_, hasTrusted := changed["Trusted"] _, hasTrusted := changed["Trusted"]
if hasPaired { if hasPaired {
if paired, ok := pairedVar.Value().(bool); ok && paired { devicePath := string(path)
devicePath := string(path) if paired, ok := pairedVar.Value().(bool); ok {
m.pendingPairingsMux.Lock() if paired {
wasPending := m.pendingPairings[devicePath] _, wasPending := m.pendingPairings.LoadAndDelete(devicePath)
if wasPending {
delete(m.pendingPairings, devicePath)
}
m.pendingPairingsMux.Unlock()
if wasPending { if wasPending {
select { select {
case m.eventQueue <- func() { case m.eventQueue <- func() {
time.Sleep(300 * time.Millisecond) time.Sleep(300 * time.Millisecond)
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath) log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
if err := m.ConnectDevice(devicePath); err != nil { if err := m.ConnectDevice(devicePath); err != nil {
log.Warnf("[Bluetooth] Auto-connect failed: %v", err) log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
}
}:
default:
} }
}:
default:
} }
} else {
m.pendingPairings.Delete(devicePath)
} }
} }
} }
@@ -430,28 +425,20 @@ func (m *Manager) notifier() {
} }
m.updateDevices() m.updateDevices()
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan BluetoothState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -484,48 +471,36 @@ func (m *Manager) snapshotState() BluetoothState {
func (m *Manager) Subscribe(id string) chan BluetoothState { func (m *Manager) Subscribe(id string) chan BluetoothState {
ch := make(chan BluetoothState, 64) ch := make(chan BluetoothState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if ch, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok {
close(ch) close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) SubscribePairing(id string) chan PairingPrompt { func (m *Manager) SubscribePairing(id string) chan PairingPrompt {
ch := make(chan PairingPrompt, 16) ch := make(chan PairingPrompt, 16)
m.pairingSubMutex.Lock() m.pairingSubscribers.Store(id, ch)
m.pairingSubscribers[id] = ch
m.pairingSubMutex.Unlock()
return ch return ch
} }
func (m *Manager) UnsubscribePairing(id string) { func (m *Manager) UnsubscribePairing(id string) {
m.pairingSubMutex.Lock() if ch, ok := m.pairingSubscribers.LoadAndDelete(id); ok {
if ch, ok := m.pairingSubscribers[id]; ok {
close(ch) close(ch)
delete(m.pairingSubscribers, id)
} }
m.pairingSubMutex.Unlock()
} }
func (m *Manager) broadcastPairingPrompt(prompt PairingPrompt) { func (m *Manager) broadcastPairingPrompt(prompt PairingPrompt) {
m.pairingSubMutex.RLock() m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool {
defer m.pairingSubMutex.RUnlock()
for _, ch := range m.pairingSubscribers {
select { select {
case ch <- prompt: case ch <- prompt:
default: default:
} }
} return true
})
} }
func (m *Manager) SubmitPairing(token string, secrets map[string]string, accept bool) error { func (m *Manager) SubmitPairing(token string, secrets map[string]string, accept bool) error {
@@ -566,17 +541,13 @@ func (m *Manager) SetPowered(powered bool) error {
} }
func (m *Manager) PairDevice(devicePath string) error { func (m *Manager) PairDevice(devicePath string) error {
m.pendingPairingsMux.Lock() m.pendingPairings.Store(devicePath, true)
m.pendingPairings[devicePath] = true
m.pendingPairingsMux.Unlock()
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath)) obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath))
err := obj.Call(device1Iface+".Pair", 0).Err err := obj.Call(device1Iface+".Pair", 0).Err
if err != nil { if err != nil {
m.pendingPairingsMux.Lock() m.pendingPairings.Delete(devicePath)
delete(m.pendingPairings, devicePath)
m.pendingPairingsMux.Unlock()
} }
return err return err
@@ -618,19 +589,17 @@ func (m *Manager) Close() {
m.agent.Close() m.agent.Close()
} }
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan BluetoothState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan BluetoothState) return true
m.subMutex.Unlock() })
m.pairingSubMutex.Lock() m.pairingSubscribers.Range(func(key string, ch chan PairingPrompt) bool {
for _, ch := range m.pairingSubscribers {
close(ch) close(ch)
} m.pairingSubscribers.Delete(key)
m.pairingSubscribers = make(map[string]chan PairingPrompt) return true
m.pairingSubMutex.Unlock() })
if m.dbusConn != nil { if m.dbusConn != nil {
m.dbusConn.Close() m.dbusConn.Close()
@@ -3,22 +3,19 @@ package bluez
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type SubscriptionBroker struct { type SubscriptionBroker struct {
mu sync.RWMutex pending syncmap.Map[string, chan PromptReply]
pending map[string]chan PromptReply requests syncmap.Map[string, PromptRequest]
requests map[string]PromptRequest
broadcastPrompt func(PairingPrompt) broadcastPrompt func(PairingPrompt)
} }
func NewSubscriptionBroker(broadcastPrompt func(PairingPrompt)) PromptBroker { func NewSubscriptionBroker(broadcastPrompt func(PairingPrompt)) PromptBroker {
return &SubscriptionBroker{ return &SubscriptionBroker{
pending: make(map[string]chan PromptReply),
requests: make(map[string]PromptRequest),
broadcastPrompt: broadcastPrompt, broadcastPrompt: broadcastPrompt,
} }
} }
@@ -30,10 +27,8 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
replyChan := make(chan PromptReply, 1) replyChan := make(chan PromptReply, 1)
b.mu.Lock() b.pending.Store(token, replyChan)
b.pending[token] = replyChan b.requests.Store(token, req)
b.requests[token] = req
b.mu.Unlock()
if b.broadcastPrompt != nil { if b.broadcastPrompt != nil {
prompt := PairingPrompt{ prompt := PairingPrompt{
@@ -53,10 +48,7 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) { func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
return PromptReply{}, fmt.Errorf("unknown token: %s", token) return PromptReply{}, fmt.Errorf("unknown token: %s", token)
} }
@@ -75,10 +67,7 @@ func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptRepl
} }
func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error { func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
return fmt.Errorf("unknown or expired token: %s", token) return fmt.Errorf("unknown or expired token: %s", token)
} }
@@ -92,8 +81,6 @@ func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
} }
func (b *SubscriptionBroker) cleanup(token string) { func (b *SubscriptionBroker) cleanup(token string) {
b.mu.Lock() b.pending.Delete(token)
delete(b.pending, token) b.requests.Delete(token)
delete(b.requests, token)
b.mu.Unlock()
} }
+4 -6
View File
@@ -3,6 +3,7 @@ package bluez
import ( import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -59,22 +60,19 @@ type PairingPrompt struct {
type Manager struct { type Manager struct {
state *BluetoothState state *BluetoothState
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan BluetoothState subscribers syncmap.Map[string, chan BluetoothState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
dbusConn *dbus.Conn dbusConn *dbus.Conn
signals chan *dbus.Signal signals chan *dbus.Signal
sigWG sync.WaitGroup sigWG sync.WaitGroup
agent *BluezAgent agent *BluezAgent
promptBroker PromptBroker promptBroker PromptBroker
pairingSubscribers map[string]chan PairingPrompt pairingSubscribers syncmap.Map[string, chan PairingPrompt]
pairingSubMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotifiedState *BluetoothState lastNotifiedState *BluetoothState
adapterPath dbus.ObjectPath adapterPath dbus.ObjectPath
pendingPairings map[string]bool pendingPairings syncmap.Map[string, bool]
pendingPairingsMux sync.Mutex
eventQueue chan func() eventQueue chan func()
eventWg sync.WaitGroup eventWg sync.WaitGroup
} }
+14 -25
View File
@@ -24,7 +24,6 @@ const (
func NewDDCBackend() (*DDCBackend, error) { func NewDDCBackend() (*DDCBackend, error) {
b := &DDCBackend{ b := &DDCBackend{
devices: make(map[string]*ddcDevice),
scanInterval: 30 * time.Second, scanInterval: 30 * time.Second,
debounceTimers: make(map[string]*time.Timer), debounceTimers: make(map[string]*time.Timer),
debouncePending: make(map[string]ddcPendingSet), debouncePending: make(map[string]ddcPendingSet),
@@ -53,10 +52,10 @@ func (b *DDCBackend) scanI2CDevices() error {
return nil return nil
} }
b.devicesMutex.Lock() b.devices.Range(func(key string, value *ddcDevice) bool {
defer b.devicesMutex.Unlock() b.devices.Delete(key)
return true
b.devices = make(map[string]*ddcDevice) })
for i := 0; i < 32; i++ { for i := 0; i < 32; i++ {
busPath := fmt.Sprintf("/dev/i2c-%d", i) busPath := fmt.Sprintf("/dev/i2c-%d", i)
@@ -64,7 +63,6 @@ func (b *DDCBackend) scanI2CDevices() error {
continue continue
} }
// Skip SMBus, GPU internal buses (e.g. AMDGPU SMU) to prevent GPU hangs
if isIgnorableI2CBus(i) { if isIgnorableI2CBus(i) {
log.Debugf("Skipping ignorable i2c-%d", i) log.Debugf("Skipping ignorable i2c-%d", i)
continue continue
@@ -77,7 +75,7 @@ func (b *DDCBackend) scanI2CDevices() error {
id := fmt.Sprintf("ddc:i2c-%d", i) id := fmt.Sprintf("ddc:i2c-%d", i)
dev.id = id dev.id = id
b.devices[id] = dev b.devices.Store(id, dev)
log.Debugf("found DDC device on i2c-%d", i) log.Debugf("found DDC device on i2c-%d", i)
} }
@@ -164,12 +162,9 @@ func (b *DDCBackend) GetDevices() ([]Device, error) {
log.Debugf("DDC scan error: %v", err) log.Debugf("DDC scan error: %v", err)
} }
b.devicesMutex.Lock() devices := make([]Device, 0)
defer b.devicesMutex.Unlock()
devices := make([]Device, 0, len(b.devices)) b.devices.Range(func(id string, dev *ddcDevice) bool {
for id, dev := range b.devices {
devices = append(devices, Device{ devices = append(devices, Device{
Class: ClassDDC, Class: ClassDDC,
ID: id, ID: id,
@@ -179,7 +174,8 @@ func (b *DDCBackend) GetDevices() ([]Device, error) {
CurrentPercent: dev.lastBrightness, CurrentPercent: dev.lastBrightness,
Backend: "ddc", Backend: "ddc",
}) })
} return true
})
return devices, nil return devices, nil
} }
@@ -189,9 +185,7 @@ func (b *DDCBackend) SetBrightness(id string, value int, exponential bool, callb
} }
func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error { func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error {
b.devicesMutex.RLock() _, ok := b.devices.Load(id)
_, ok := b.devices[id]
b.devicesMutex.RUnlock()
if !ok { if !ok {
return fmt.Errorf("device not found: %s", id) return fmt.Errorf("device not found: %s", id)
@@ -202,8 +196,6 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
} }
b.debounceMutex.Lock() b.debounceMutex.Lock()
defer b.debounceMutex.Unlock()
b.debouncePending[id] = ddcPendingSet{ b.debouncePending[id] = ddcPendingSet{
percent: value, percent: value,
callback: callback, callback: callback,
@@ -234,14 +226,13 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
} }
}) })
} }
b.debounceMutex.Unlock()
return nil return nil
} }
func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) error { func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) error {
b.devicesMutex.RLock() dev, ok := b.devices.Load(id)
dev, ok := b.devices[id]
b.devicesMutex.RUnlock()
if !ok { if !ok {
return fmt.Errorf("device not found: %s", id) return fmt.Errorf("device not found: %s", id)
@@ -266,9 +257,8 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
return fmt.Errorf("get current capability: %w", err) return fmt.Errorf("get current capability: %w", err)
} }
max = cap.max max = cap.max
b.devicesMutex.Lock()
dev.max = max dev.max = max
b.devicesMutex.Unlock() b.devices.Store(id, dev)
} }
if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, value); err != nil { if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, value); err != nil {
@@ -277,10 +267,9 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
log.Debugf("set %s to %d/%d", id, value, max) log.Debugf("set %s to %d/%d", id, value, max)
b.devicesMutex.Lock()
dev.max = max dev.max = max
dev.lastBrightness = value dev.lastBrightness = value
b.devicesMutex.Unlock() b.devices.Store(id, dev)
return nil return nil
} }
+5 -14
View File
@@ -15,10 +15,8 @@ func NewManager() (*Manager, error) {
func NewManagerWithOptions(exponential bool) (*Manager, error) { func NewManagerWithOptions(exponential bool) (*Manager, error) {
m := &Manager{ m := &Manager{
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate), exponential: exponential,
stopChan: make(chan struct{}),
exponential: exponential,
} }
go m.initLogind() go m.initLogind()
@@ -360,20 +358,13 @@ func (m *Manager) broadcastDeviceUpdate(deviceID string) {
update := DeviceUpdate{Device: *targetDevice} update := DeviceUpdate{Device: *targetDevice}
m.subMutex.RLock()
defer m.subMutex.RUnlock()
if len(m.updateSubscribers) == 0 {
log.Debugf("No update subscribers for device: %s", deviceID)
return
}
log.Debugf("Broadcasting device update: %s at %d%%", deviceID, targetDevice.CurrentPercent) log.Debugf("Broadcasting device update: %s at %d%%", deviceID, targetDevice.CurrentPercent)
for _, ch := range m.updateSubscribers { m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool {
select { select {
case ch <- update: case ch <- update:
default: default:
} }
} return true
})
} }
+13 -22
View File
@@ -13,9 +13,8 @@ import (
func NewSysfsBackend() (*SysfsBackend, error) { func NewSysfsBackend() (*SysfsBackend, error) {
b := &SysfsBackend{ b := &SysfsBackend{
basePath: "/sys/class", basePath: "/sys/class",
classes: []string{"backlight", "leds"}, classes: []string{"backlight", "leds"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := b.scanDevices(); err != nil { if err := b.scanDevices(); err != nil {
@@ -26,9 +25,6 @@ func NewSysfsBackend() (*SysfsBackend, error) {
} }
func (b *SysfsBackend) scanDevices() error { func (b *SysfsBackend) scanDevices() error {
b.deviceCacheMutex.Lock()
defer b.deviceCacheMutex.Unlock()
for _, class := range b.classes { for _, class := range b.classes {
classPath := filepath.Join(b.basePath, class) classPath := filepath.Join(b.basePath, class)
entries, err := os.ReadDir(classPath) entries, err := os.ReadDir(classPath)
@@ -68,13 +64,13 @@ func (b *SysfsBackend) scanDevices() error {
} }
deviceID := fmt.Sprintf("%s:%s", class, entry.Name()) deviceID := fmt.Sprintf("%s:%s", class, entry.Name())
b.deviceCache[deviceID] = &sysfsDevice{ b.deviceCache.Store(deviceID, &sysfsDevice{
class: deviceClass, class: deviceClass,
id: deviceID, id: deviceID,
name: entry.Name(), name: entry.Name(),
maxBrightness: maxBrightness, maxBrightness: maxBrightness,
minValue: minValue, minValue: minValue,
} })
log.Debugf("found %s device: %s (max=%d)", class, entry.Name(), maxBrightness) log.Debugf("found %s device: %s (max=%d)", class, entry.Name(), maxBrightness)
} }
@@ -106,19 +102,16 @@ func shouldSuppressDevice(name string) bool {
} }
func (b *SysfsBackend) GetDevices() ([]Device, error) { func (b *SysfsBackend) GetDevices() ([]Device, error) {
b.deviceCacheMutex.RLock() devices := make([]Device, 0)
defer b.deviceCacheMutex.RUnlock()
devices := make([]Device, 0, len(b.deviceCache)) b.deviceCache.Range(func(key string, dev *sysfsDevice) bool {
for _, dev := range b.deviceCache {
if shouldSuppressDevice(dev.name) { if shouldSuppressDevice(dev.name) {
continue return true
} }
parts := strings.SplitN(dev.id, ":", 2) parts := strings.SplitN(dev.id, ":", 2)
if len(parts) != 2 { if len(parts) != 2 {
continue return true
} }
class := parts[0] class := parts[0]
@@ -130,13 +123,13 @@ func (b *SysfsBackend) GetDevices() ([]Device, error) {
brightnessData, err := os.ReadFile(brightnessPath) brightnessData, err := os.ReadFile(brightnessPath)
if err != nil { if err != nil {
log.Debugf("failed to read brightness for %s: %v", dev.id, err) log.Debugf("failed to read brightness for %s: %v", dev.id, err)
continue return true
} }
current, err := strconv.Atoi(strings.TrimSpace(string(brightnessData))) current, err := strconv.Atoi(strings.TrimSpace(string(brightnessData)))
if err != nil { if err != nil {
log.Debugf("failed to parse brightness for %s: %v", dev.id, err) log.Debugf("failed to parse brightness for %s: %v", dev.id, err)
continue return true
} }
percent := b.ValueToPercent(current, dev, false) percent := b.ValueToPercent(current, dev, false)
@@ -150,16 +143,14 @@ func (b *SysfsBackend) GetDevices() ([]Device, error) {
CurrentPercent: percent, CurrentPercent: percent,
Backend: "sysfs", Backend: "sysfs",
}) })
} return true
})
return devices, nil return devices, nil
} }
func (b *SysfsBackend) GetDevice(id string) (*sysfsDevice, error) { func (b *SysfsBackend) GetDevice(id string) (*sysfsDevice, error) {
b.deviceCacheMutex.RLock() dev, ok := b.deviceCache.Load(id)
defer b.deviceCacheMutex.RUnlock()
dev, ok := b.deviceCache[id]
if !ok { if !ok {
return nil, fmt.Errorf("device not found: %s", id) return nil, fmt.Errorf("device not found: %s", id)
} }
@@ -31,9 +31,8 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn) mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight"}, classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -41,13 +40,11 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
} }
m := &Manager{ m := &Manager{
logindBackend: mockLogind, logindBackend: mockLogind,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
} }
m.state = State{ m.state = State{
@@ -105,9 +102,8 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn) mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight"}, classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -115,13 +111,11 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
} }
m := &Manager{ m := &Manager{
logindBackend: mockLogind, logindBackend: mockLogind,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
} }
m.state = State{ m.state = State{
@@ -175,9 +169,8 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
} }
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight"}, classes: []string{"backlight"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -185,13 +178,11 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
} }
m := &Manager{ m := &Manager{
logindBackend: nil, logindBackend: nil,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: false, logindReady: false,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
} }
m.state = State{ m.state = State{
@@ -240,9 +231,8 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
mockLogind := NewLogindBackendWithConn(mockConn) mockLogind := NewLogindBackendWithConn(mockConn)
sysfs := &SysfsBackend{ sysfs := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"leds"}, classes: []string{"leds"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := sysfs.scanDevices(); err != nil { if err := sysfs.scanDevices(); err != nil {
@@ -250,13 +240,11 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
} }
m := &Manager{ m := &Manager{
logindBackend: mockLogind, logindBackend: mockLogind,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
sysfsReady: true, sysfsReady: true,
subscribers: make(map[string]chan State), stopChan: make(chan struct{}),
updateSubscribers: make(map[string]chan DeviceUpdate),
stopChan: make(chan struct{}),
} }
m.state = State{ m.state = State{
@@ -160,26 +160,21 @@ func TestSysfsBackend_ScanDevices(t *testing.T) {
} }
b := &SysfsBackend{ b := &SysfsBackend{
basePath: tmpDir, basePath: tmpDir,
classes: []string{"backlight", "leds"}, classes: []string{"backlight", "leds"},
deviceCache: make(map[string]*sysfsDevice),
} }
if err := b.scanDevices(); err != nil { if err := b.scanDevices(); err != nil {
t.Fatalf("scanDevices() error = %v", err) t.Fatalf("scanDevices() error = %v", err)
} }
if len(b.deviceCache) != 2 {
t.Errorf("expected 2 devices, got %d", len(b.deviceCache))
}
backlightID := "backlight:test_backlight" backlightID := "backlight:test_backlight"
if _, ok := b.deviceCache[backlightID]; !ok { if _, ok := b.deviceCache.Load(backlightID); !ok {
t.Errorf("backlight device not found") t.Errorf("backlight device not found")
} }
ledID := "leds:test_led" ledID := "leds:test_led"
if _, ok := b.deviceCache[ledID]; !ok { if _, ok := b.deviceCache.Load(ledID); !ok {
t.Errorf("LED device not found") t.Errorf("LED device not found")
} }
} }
+28 -36
View File
@@ -3,6 +3,8 @@ package brightness
import ( import (
"sync" "sync"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type DeviceClass string type DeviceClass string
@@ -51,9 +53,8 @@ type Manager struct {
stateMutex sync.RWMutex stateMutex sync.RWMutex
state State state State
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
updateSubscribers map[string]chan DeviceUpdate updateSubscribers syncmap.Map[string, chan DeviceUpdate]
subMutex sync.RWMutex
broadcastMutex sync.Mutex broadcastMutex sync.Mutex
broadcastTimer *time.Timer broadcastTimer *time.Timer
@@ -67,8 +68,7 @@ type SysfsBackend struct {
basePath string basePath string
classes []string classes []string
deviceCache map[string]*sysfsDevice deviceCache syncmap.Map[string, *sysfsDevice]
deviceCacheMutex sync.RWMutex
} }
type sysfsDevice struct { type sysfsDevice struct {
@@ -80,8 +80,7 @@ type sysfsDevice struct {
} }
type DDCBackend struct { type DDCBackend struct {
devices map[string]*ddcDevice devices syncmap.Map[string, *ddcDevice]
devicesMutex sync.RWMutex
scanMutex sync.Mutex scanMutex sync.Mutex
lastScan time.Time lastScan time.Time
@@ -121,36 +120,31 @@ type SetBrightnessParams struct {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 16) ch := make(chan State, 16)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok { if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch) close(val)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) SubscribeUpdates(id string) chan DeviceUpdate { func (m *Manager) SubscribeUpdates(id string) chan DeviceUpdate {
ch := make(chan DeviceUpdate, 16) ch := make(chan DeviceUpdate, 16)
m.subMutex.Lock() m.updateSubscribers.Store(id, ch)
m.updateSubscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) UnsubscribeUpdates(id string) { func (m *Manager) UnsubscribeUpdates(id string) {
m.subMutex.Lock() if val, ok := m.updateSubscribers.LoadAndDelete(id); ok {
if ch, ok := m.updateSubscribers[id]; ok { close(val)
close(ch)
delete(m.updateSubscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) NotifySubscribers() { func (m *Manager) NotifySubscribers() {
@@ -158,15 +152,13 @@ func (m *Manager) NotifySubscribers() {
state := m.state state := m.state
m.stateMutex.RUnlock() m.stateMutex.RUnlock()
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
defer m.subMutex.RUnlock()
for _, ch := range m.subscribers {
select { select {
case ch <- state: case ch <- state:
default: default:
} }
} return true
})
} }
func (m *Manager) GetState() State { func (m *Manager) GetState() State {
@@ -178,16 +170,16 @@ func (m *Manager) GetState() State {
func (m *Manager) Close() { func (m *Manager) Close() {
close(m.stopChan) close(m.stopChan)
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
for _, ch := range m.updateSubscribers { })
m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool {
close(ch) close(ch)
} m.updateSubscribers.Delete(key)
m.updateSubscribers = make(map[string]chan DeviceUpdate) return true
m.subMutex.Unlock() })
if m.logindBackend != nil { if m.logindBackend != nil {
m.logindBackend.Close() m.logindBackend.Close()
+28 -32
View File
@@ -35,13 +35,11 @@ func NewManager() (*Manager, error) {
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: client, client: client,
baseURL: baseURL, baseURL: baseURL,
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
subMutex: sync.RWMutex{},
} }
if err := m.updateState(); err != nil { if err := m.updateState(); err != nil {
@@ -142,28 +140,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan CUPSState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -199,10 +190,14 @@ func (m *Manager) snapshotState() CUPSState {
func (m *Manager) Subscribe(id string) chan CUPSState { func (m *Manager) Subscribe(id string) chan CUPSState {
ch := make(chan CUPSState, 64) ch := make(chan CUPSState, 64)
m.subMutex.Lock()
wasEmpty := len(m.subscribers) == 0 wasEmpty := true
m.subscribers[id] = ch m.subscribers.Range(func(key string, ch chan CUPSState) bool {
m.subMutex.Unlock() wasEmpty = false
return false
})
m.subscribers.Store(id, ch)
if wasEmpty && m.subscription != nil { if wasEmpty && m.subscription != nil {
if err := m.subscription.Start(); err != nil { if err := m.subscription.Start(); err != nil {
@@ -217,13 +212,15 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
isEmpty := len(m.subscribers) == 0
m.subMutex.Unlock() isEmpty := true
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
isEmpty = false
return false
})
if isEmpty && m.subscription != nil { if isEmpty && m.subscription != nil {
m.subscription.Stop() m.subscription.Stop()
@@ -241,12 +238,11 @@ func (m *Manager) Close() {
m.eventWG.Wait() m.eventWG.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan CUPSState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan CUPSState) return true
m.subMutex.Unlock() })
} }
func stateChanged(old, new *CUPSState) bool { func stateChanged(old, new *CUPSState) bool {
+31 -19
View File
@@ -13,10 +13,9 @@ func TestNewManager(t *testing.T) {
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: nil, client: nil,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
assert.NotNil(t, m) assert.NotNil(t, m)
@@ -35,10 +34,9 @@ func TestManager_GetState(t *testing.T) {
}, },
}, },
}, },
client: mockClient, client: mockClient,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
state := m.GetState() state := m.GetState()
@@ -53,18 +51,28 @@ func TestManager_Subscribe(t *testing.T) {
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: mockClient, client: mockClient,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
ch := m.Subscribe("test-client") ch := m.Subscribe("test-client")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Equal(t, 1, len(m.subscribers))
count := 0
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
count++
return true
})
assert.Equal(t, 1, count)
m.Unsubscribe("test-client") m.Unsubscribe("test-client")
assert.Equal(t, 0, len(m.subscribers)) count = 0
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
count++
return true
})
assert.Equal(t, 0, count)
} }
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
@@ -74,10 +82,9 @@ func TestManager_Close(t *testing.T) {
state: &CUPSState{ state: &CUPSState{
Printers: make(map[string]*Printer), Printers: make(map[string]*Printer),
}, },
client: mockClient, client: mockClient,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
subscribers: make(map[string]chan CUPSState),
} }
m.eventWG.Add(1) m.eventWG.Add(1)
@@ -93,7 +100,12 @@ func TestManager_Close(t *testing.T) {
}() }()
m.Close() m.Close()
assert.Equal(t, 0, len(m.subscribers)) count := 0
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
count++
return true
})
assert.Equal(t, 0, count)
} }
func TestStateChanged(t *testing.T) { func TestStateChanged(t *testing.T) {
+2 -2
View File
@@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type CUPSState struct { type CUPSState struct {
@@ -39,8 +40,7 @@ type Manager struct {
client CUPSClientInterface client CUPSClientInterface
subscription SubscriptionManagerInterface subscription SubscriptionManagerInterface
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan CUPSState subscribers syncmap.Map[string, chan CUPSState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
eventWG sync.WaitGroup eventWG sync.WaitGroup
dirty chan struct{} dirty chan struct{}
+51 -69
View File
@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"time" "time"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
@@ -14,13 +14,12 @@ func NewManager(display *wlclient.Display) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
ctx: display.Context(), ctx: display.Context(),
outputs: make(map[uint32]*outputState),
cmdq: make(chan cmd, 128), cmdq: make(chan cmd, 128),
outputSetupReq: make(chan uint32, 16), outputSetupReq: make(chan uint32, 16),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
layouts: make([]string, 0), layouts: make([]string, 0),
} }
if err := m.setupRegistry(); err != nil { if err := m.setupRegistry(); err != nil {
@@ -56,10 +55,7 @@ func (m *Manager) waylandActor() {
case c := <-m.cmdq: case c := <-m.cmdq:
c.fn() c.fn()
case outputID := <-m.outputSetupReq: case outputID := <-m.outputSetupReq:
m.outputsMutex.RLock() out, exists := m.outputs.Load(outputID)
out, exists := m.outputs[outputID]
m.outputsMutex.RUnlock()
if !exists { if !exists {
log.Warnf("DWL: Output %d no longer exists, skipping setup", outputID) log.Warnf("DWL: Output %d no longer exists, skipping setup", outputID)
continue continue
@@ -104,8 +100,8 @@ func (m *Manager) setupRegistry() error {
log.Infof("DWL: found %s", dwl_ipc.ZdwlIpcManagerV2InterfaceName) log.Infof("DWL: found %s", dwl_ipc.ZdwlIpcManagerV2InterfaceName)
manager := dwl_ipc.NewZdwlIpcManagerV2(m.ctx) manager := dwl_ipc.NewZdwlIpcManagerV2(m.ctx)
version := e.Version version := e.Version
if version > 1 { if version > 2 {
version = 1 version = 2
} }
if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil { if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil {
dwlMgr = manager dwlMgr = manager
@@ -156,9 +152,7 @@ func (m *Manager) setupRegistry() error {
outputs = append(outputs, output) outputs = append(outputs, output)
outputRegNames[outputID] = e.Name outputRegNames[outputID] = e.Name
m.outputsMutex.Lock() m.outputs.Store(outputID, outState)
m.outputs[outputID] = outState
m.outputsMutex.Unlock()
if m.manager != nil { if m.manager != nil {
select { select {
@@ -176,17 +170,16 @@ func (m *Manager) setupRegistry() error {
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) { registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
m.post(func() { m.post(func() {
m.outputsMutex.Lock()
var outToRelease *outputState var outToRelease *outputState
for id, out := range m.outputs { m.outputs.Range(func(id uint32, out *outputState) bool {
if out.registryName == e.Name { if out.registryName == e.Name {
log.Infof("DWL: Output %d removed", id) log.Infof("DWL: Output %d removed", id)
outToRelease = out outToRelease = out
delete(m.outputs, id) m.outputs.Delete(id)
break return false
} }
} return true
m.outputsMutex.Unlock() })
if outToRelease != nil { if outToRelease != nil {
if ipcOut, ok := outToRelease.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok && ipcOut != nil { if ipcOut, ok := outToRelease.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok && ipcOut != nil {
@@ -236,14 +229,11 @@ func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclien
return fmt.Errorf("failed to get dwl output: %w", err) return fmt.Errorf("failed to get dwl output: %w", err)
} }
m.outputsMutex.Lock() outState, exists := m.outputs.Load(output.ID())
outState, exists := m.outputs[output.ID()]
if !exists { if !exists {
m.outputsMutex.Unlock()
return fmt.Errorf("output state not found for id %d", output.ID()) return fmt.Errorf("output state not found for id %d", output.ID())
} }
outState.ipcOutput = ipcOutput outState.ipcOutput = ipcOutput
m.outputsMutex.Unlock()
ipcOutput.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) { ipcOutput.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
outState.active = e.Active outState.active = e.Active
@@ -292,6 +282,14 @@ func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclien
outState.layoutSymbol = e.Layout outState.layoutSymbol = e.Layout
}) })
ipcOutput.SetKbLayoutHandler(func(e dwl_ipc.ZdwlIpcOutputV2KbLayoutEvent) {
outState.kbLayout = e.KbLayout
})
ipcOutput.SetKeymodeHandler(func(e dwl_ipc.ZdwlIpcOutputV2KeymodeEvent) {
outState.keymode = e.Keymode
})
ipcOutput.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) { ipcOutput.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
m.updateState() m.updateState()
}) })
@@ -300,11 +298,10 @@ func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclien
} }
func (m *Manager) updateState() { func (m *Manager) updateState() {
m.outputsMutex.RLock()
outputs := make(map[string]*OutputState) outputs := make(map[string]*OutputState)
activeOutput := "" activeOutput := ""
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
@@ -321,13 +318,15 @@ func (m *Manager) updateState() {
LayoutSymbol: out.layoutSymbol, LayoutSymbol: out.layoutSymbol,
Title: out.title, Title: out.title,
AppID: out.appID, AppID: out.appID,
KbLayout: out.kbLayout,
Keymode: out.keymode,
} }
if out.active != 0 { if out.active != 0 {
activeOutput = name activeOutput = name
} }
} return true
m.outputsMutex.RUnlock() })
newState := State{ newState := State{
Outputs: outputs, Outputs: outputs,
@@ -365,14 +364,6 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
subCount := len(m.subscribers)
m.subMutex.RUnlock()
if subCount == 0 {
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
@@ -381,15 +372,14 @@ func (m *Manager) notifier() {
continue continue
} }
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
log.Warn("DWL: subscriber channel full, dropping update") log.Warn("DWL: subscriber channel full, dropping update")
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -407,11 +397,9 @@ func (m *Manager) ensureOutputSetup(out *outputState) error {
} }
func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32) error { func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32) error {
m.outputsMutex.RLock() availableOutputs := make([]string, 0)
availableOutputs := make([]string, 0, len(m.outputs))
var targetOut *outputState var targetOut *outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
@@ -419,10 +407,10 @@ func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32
availableOutputs = append(availableOutputs, name) availableOutputs = append(availableOutputs, name)
if name == outputName { if name == outputName {
targetOut = out targetOut = out
break return false
} }
} return true
m.outputsMutex.RUnlock() })
if targetOut == nil { if targetOut == nil {
return fmt.Errorf("output not found: %s (available: %v)", outputName, availableOutputs) return fmt.Errorf("output not found: %s (available: %v)", outputName, availableOutputs)
@@ -444,20 +432,18 @@ func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32
} }
func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint32) error { func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint32) error {
m.outputsMutex.RLock()
var targetOut *outputState var targetOut *outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
} }
if name == outputName { if name == outputName {
targetOut = out targetOut = out
break return false
} }
} return true
m.outputsMutex.RUnlock() })
if targetOut == nil { if targetOut == nil {
return fmt.Errorf("output not found: %s", outputName) return fmt.Errorf("output not found: %s", outputName)
@@ -479,20 +465,18 @@ func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint3
} }
func (m *Manager) SetLayout(outputName string, index uint32) error { func (m *Manager) SetLayout(outputName string, index uint32) error {
m.outputsMutex.RLock()
var targetOut *outputState var targetOut *outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name name := out.name
if name == "" { if name == "" {
name = fmt.Sprintf("output-%d", out.id) name = fmt.Sprintf("output-%d", out.id)
} }
if name == outputName { if name == outputName {
targetOut = out targetOut = out
break return false
} }
} return true
m.outputsMutex.RUnlock() })
if targetOut == nil { if targetOut == nil {
return fmt.Errorf("output not found: %s", outputName) return fmt.Errorf("output not found: %s", outputName)
@@ -518,21 +502,19 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.outputsMutex.Lock() m.outputs.Range(func(key uint32, out *outputState) bool {
for _, out := range m.outputs {
if ipcOut, ok := out.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok { if ipcOut, ok := out.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok {
ipcOut.Release() ipcOut.Release()
} }
} m.outputs.Delete(key)
m.outputs = make(map[uint32]*outputState) return true
m.outputsMutex.Unlock() })
if mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2); ok { if mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2); ok {
mgr.Release() mgr.Release()
+19 -13
View File
@@ -3,7 +3,8 @@ package dwl
import ( import (
"sync" "sync"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type TagState struct { type TagState struct {
@@ -21,6 +22,8 @@ type OutputState struct {
LayoutSymbol string `json:"layoutSymbol"` LayoutSymbol string `json:"layoutSymbol"`
Title string `json:"title"` Title string `json:"title"`
AppID string `json:"appId"` AppID string `json:"appId"`
KbLayout string `json:"kbLayout"`
Keymode string `json:"keymode"`
} }
type State struct { type State struct {
@@ -40,8 +43,7 @@ type Manager struct {
registry *wlclient.Registry registry *wlclient.Registry
manager interface{} manager interface{}
outputs map[uint32]*outputState outputs syncmap.Map[uint32, *outputState]
outputsMutex sync.RWMutex
tagCount uint32 tagCount uint32
layouts []string layouts []string
@@ -52,8 +54,7 @@ type Manager struct {
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -74,6 +75,8 @@ type outputState struct {
layoutSymbol string layoutSymbol string
title string title string
appID string appID string
kbLayout string
keymode string
} }
func (m *Manager) GetState() State { func (m *Manager) GetState() State {
@@ -92,19 +95,16 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {
@@ -151,6 +151,12 @@ func stateChanged(old, new *State) bool {
if oldOut.AppID != newOut.AppID { if oldOut.AppID != newOut.AppID {
return true return true
} }
if oldOut.KbLayout != newOut.KbLayout {
return true
}
if oldOut.Keymode != newOut.Keymode {
return true
}
if len(oldOut.Tags) != len(newOut.Tags) { if len(oldOut.Tags) != len(newOut.Tags) {
return true return true
} }
+9 -12
View File
@@ -47,10 +47,9 @@ func TestHandleRequest(t *testing.T) {
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{ m := &Manager{
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: true}, state: State{Available: true, CapsLock: true},
subscribers: make(map[string]chan State), closeChan: make(chan struct{}),
closeChan: make(chan struct{}),
} }
conn := newMockNetConn() conn := newMockNetConn()
@@ -77,10 +76,9 @@ func TestHandleRequest(t *testing.T) {
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{ m := &Manager{
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State), closeChan: make(chan struct{}),
closeChan: make(chan struct{}),
} }
conn := newMockNetConn() conn := newMockNetConn()
@@ -107,10 +105,9 @@ func TestHandleGetState(t *testing.T) {
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{ m := &Manager{
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State), closeChan: make(chan struct{}),
closeChan: make(chan struct{}),
} }
conn := newMockNetConn() conn := newMockNetConn()
+25 -29
View File
@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
evdev "github.com/holoplot/go-evdev" evdev "github.com/holoplot/go-evdev"
) )
@@ -35,8 +36,7 @@ type Manager struct {
monitoredPaths map[string]bool monitoredPaths map[string]bool
state State state State
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
closeChan chan struct{} closeChan chan struct{}
closeOnce sync.Once closeOnce sync.Once
watcher *fsnotify.Watcher watcher *fsnotify.Watcher
@@ -69,9 +69,9 @@ func NewManager() (*Manager, error) {
devices: devices, devices: devices,
monitoredPaths: monitoredPaths, monitoredPaths: monitoredPaths,
state: State{Available: true, CapsLock: initialCapsLock}, state: State{Available: true, CapsLock: initialCapsLock},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
watcher: watcher, watcher: watcher,
} }
for i, device := range devices { for i, device := range devices {
@@ -145,9 +145,18 @@ func isKeyboard(device EvdevDevice) bool {
return true return true
case strings.Contains(name, "input") && strings.Contains(name, "key"): case strings.Contains(name, "input") && strings.Contains(name, "key"):
return true return true
default: }
keyStates, err := device.State(evKeyType)
if err != nil {
return false return false
} }
hasKeyA := len(keyStates) > 30
hasKeyZ := len(keyStates) > 44
hasEnter := len(keyStates) > 28
return hasKeyA && hasKeyZ && hasEnter && len(keyStates) > 100
} }
func (m *Manager) watchForNewKeyboards() { func (m *Manager) watchForNewKeyboards() {
@@ -323,37 +332,25 @@ func (m *Manager) GetState() State {
} }
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
m.subMutex.Lock()
defer m.subMutex.Unlock()
ch := make(chan State, 16) ch := make(chan State, 16)
m.subscribers[id] = ch m.subscribers.Store(id, ch)
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
defer m.subMutex.Unlock() close(val)
ch, ok := m.subscribers[id]
if !ok {
return
} }
close(ch)
delete(m.subscribers, id)
} }
func (m *Manager) notifySubscribers(state State) { func (m *Manager) notifySubscribers(state State) {
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
defer m.subMutex.RUnlock()
for _, ch := range m.subscribers {
select { select {
case ch <- state: case ch <- state:
default: default:
} }
} return true
})
} }
func (m *Manager) Close() { func (m *Manager) Close() {
@@ -375,12 +372,11 @@ func (m *Manager) Close() {
} }
m.devicesMutex.Unlock() m.devicesMutex.Unlock()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for id, ch := range m.subscribers {
close(ch) close(ch)
delete(m.subscribers, id) m.subscribers.Delete(key)
} return true
m.subMutex.Unlock() })
}) })
} }
+37 -22
View File
@@ -16,10 +16,9 @@ func TestManager_Creation(t *testing.T) {
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{ m := &Manager{
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State), closeChan: make(chan struct{}),
closeChan: make(chan struct{}),
} }
assert.NotNil(t, m) assert.NotNil(t, m)
@@ -32,10 +31,9 @@ func TestManager_Creation(t *testing.T) {
mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe()
m := &Manager{ m := &Manager{
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: true}, state: State{Available: true, CapsLock: true},
subscribers: make(map[string]chan State), closeChan: make(chan struct{}),
closeChan: make(chan struct{}),
} }
assert.NotNil(t, m) assert.NotNil(t, m)
@@ -52,7 +50,6 @@ func TestManager_GetState(t *testing.T) {
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool), monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
@@ -69,13 +66,17 @@ func TestManager_Subscribe(t *testing.T) {
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool), monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
ch := m.Subscribe("test-client") ch := m.Subscribe("test-client")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Len(t, m.subscribers, 1) count := 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 1, count)
} }
func TestManager_Unsubscribe(t *testing.T) { func TestManager_Unsubscribe(t *testing.T) {
@@ -86,15 +87,24 @@ func TestManager_Unsubscribe(t *testing.T) {
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool), monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
ch := m.Subscribe("test-client") ch := m.Subscribe("test-client")
assert.Len(t, m.subscribers, 1) count := 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 1, count)
m.Unsubscribe("test-client") m.Unsubscribe("test-client")
assert.Len(t, m.subscribers, 0) count = 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 0, count)
select { select {
case _, ok := <-ch: case _, ok := <-ch:
@@ -112,7 +122,6 @@ func TestManager_UpdateCapsLock(t *testing.T) {
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool), monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
@@ -148,7 +157,6 @@ func TestManager_Close(t *testing.T) {
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool), monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
@@ -171,7 +179,12 @@ func TestManager_Close(t *testing.T) {
t.Error("channel 2 should be closed") t.Error("channel 2 should be closed")
} }
assert.Len(t, m.subscribers, 0) count := 0
m.subscribers.Range(func(key string, ch chan State) bool {
count++
return true
})
assert.Equal(t, 0, count)
m.Close() m.Close()
} }
@@ -194,6 +207,10 @@ func TestIsKeyboard(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t) mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().Name().Return(tt.devName, nil).Once() mockDevice.EXPECT().Name().Return(tt.devName, nil).Once()
if !tt.expected {
mockDevice.EXPECT().State(evdev.EvType(evKeyType)).Return(evdev.StateMap{}, nil).Maybe()
}
result := isKeyboard(mockDevice) result := isKeyboard(mockDevice)
assert.Equal(t, tt.expected, result) assert.Equal(t, tt.expected, result)
}) })
@@ -226,10 +243,9 @@ func TestManager_MonitorDevice(t *testing.T) {
mockDevice.EXPECT().Close().Return(nil).Maybe() mockDevice.EXPECT().Close().Return(nil).Maybe()
m := &Manager{ m := &Manager{
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State), closeChan: make(chan struct{}),
closeChan: make(chan struct{}),
} }
ch := m.Subscribe("test") ch := m.Subscribe("test")
@@ -272,7 +288,6 @@ func TestNotifySubscribers(t *testing.T) {
devices: []EvdevDevice{mockDevice}, devices: []EvdevDevice{mockDevice},
monitoredPaths: make(map[string]bool), monitoredPaths: make(map[string]bool),
state: State{Available: true, CapsLock: false}, state: State{Available: true, CapsLock: false},
subscribers: make(map[string]chan State),
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
+110 -107
View File
@@ -6,21 +6,46 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
func CheckCapability() bool {
display, err := wlclient.Connect("")
if err != nil {
return false
}
defer display.Destroy()
registry, err := display.GetRegistry()
if err != nil {
return false
}
defer registry.Destroy()
found := false
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
if e.Interface == ext_workspace.ExtWorkspaceManagerV1InterfaceName {
found = true
}
})
// Roundtrip to ensure all registry events are processed
if err := display.Roundtrip(); err != nil {
return false
}
return found
}
func NewManager(display *wlclient.Display) (*Manager, error) { func NewManager(display *wlclient.Display) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
ctx: display.Context(), ctx: display.Context(),
outputs: make(map[uint32]*wlclient.Output), cmdq: make(chan cmd, 128),
outputNames: make(map[uint32]string), stopChan: make(chan struct{}),
groups: make(map[uint32]*workspaceGroupState),
workspaces: make(map[uint32]*workspaceState), dirty: make(chan struct{}, 1),
cmdq: make(chan cmd, 128),
stopChan: make(chan struct{}),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1),
} }
m.wg.Add(1) m.wg.Add(1)
@@ -77,10 +102,11 @@ func (m *Manager) setupRegistry() error {
outputID := output.ID() outputID := output.ID()
output.SetNameHandler(func(ev wlclient.OutputNameEvent) { output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
m.outputsMutex.Lock() m.outputNames.Store(outputID, ev.Name)
m.outputNames[outputID] = ev.Name
m.outputsMutex.Unlock()
log.Debugf("ExtWorkspace: Output %d (%s) name received", outputID, ev.Name) log.Debugf("ExtWorkspace: Output %d (%s) name received", outputID, ev.Name)
m.post(func() {
m.updateState()
})
}) })
} }
return return
@@ -139,9 +165,7 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
workspaceIDs: make([]uint32, 0), workspaceIDs: make([]uint32, 0),
} }
m.groupsMutex.Lock() m.groups.Store(groupID, group)
m.groups[groupID] = group
m.groupsMutex.Unlock()
handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1CapabilitiesEvent) { handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1CapabilitiesEvent) {
log.Debugf("ExtWorkspace: Group %d capabilities: %d", groupID, e.Capabilities) log.Debugf("ExtWorkspace: Group %d capabilities: %d", groupID, e.Capabilities)
@@ -171,11 +195,9 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
log.Debugf("ExtWorkspace: Group %d workspace enter (workspace=%d)", groupID, workspaceID) log.Debugf("ExtWorkspace: Group %d workspace enter (workspace=%d)", groupID, workspaceID)
m.post(func() { m.post(func() {
m.workspacesMutex.Lock() if ws, ok := m.workspaces.Load(workspaceID); ok {
if ws, exists := m.workspaces[workspaceID]; exists {
ws.groupID = groupID ws.groupID = groupID
} }
m.workspacesMutex.Unlock()
group.workspaceIDs = append(group.workspaceIDs, workspaceID) group.workspaceIDs = append(group.workspaceIDs, workspaceID)
m.updateState() m.updateState()
@@ -187,11 +209,9 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
log.Debugf("ExtWorkspace: Group %d workspace leave (workspace=%d)", groupID, workspaceID) log.Debugf("ExtWorkspace: Group %d workspace leave (workspace=%d)", groupID, workspaceID)
m.post(func() { m.post(func() {
m.workspacesMutex.Lock() if ws, ok := m.workspaces.Load(workspaceID); ok {
if ws, exists := m.workspaces[workspaceID]; exists {
ws.groupID = 0 ws.groupID = 0
} }
m.workspacesMutex.Unlock()
for i, id := range group.workspaceIDs { for i, id := range group.workspaceIDs {
if id == workspaceID { if id == workspaceID {
@@ -209,9 +229,7 @@ func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1Work
m.post(func() { m.post(func() {
group.removed = true group.removed = true
m.groupsMutex.Lock() m.groups.Delete(groupID)
delete(m.groups, groupID)
m.groupsMutex.Unlock()
m.wlMutex.Lock() m.wlMutex.Lock()
handle.Destroy() handle.Destroy()
@@ -234,9 +252,7 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
coordinates: make([]uint32, 0), coordinates: make([]uint32, 0),
} }
m.workspacesMutex.Lock() m.workspaces.Store(workspaceID, ws)
m.workspaces[workspaceID] = ws
m.workspacesMutex.Unlock()
handle.SetIdHandler(func(e ext_workspace.ExtWorkspaceHandleV1IdEvent) { handle.SetIdHandler(func(e ext_workspace.ExtWorkspaceHandleV1IdEvent) {
log.Debugf("ExtWorkspace: Workspace %d id: %s", workspaceID, e.Id) log.Debugf("ExtWorkspace: Workspace %d id: %s", workspaceID, e.Id)
@@ -290,9 +306,7 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
m.post(func() { m.post(func() {
ws.removed = true ws.removed = true
m.workspacesMutex.Lock() m.workspaces.Delete(workspaceID)
delete(m.workspaces, workspaceID)
m.workspacesMutex.Unlock()
m.wlMutex.Lock() m.wlMutex.Lock()
handle.Destroy() handle.Destroy()
@@ -304,32 +318,27 @@ func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1Workspace
} }
func (m *Manager) updateState() { func (m *Manager) updateState() {
m.groupsMutex.RLock()
m.workspacesMutex.RLock()
groups := make([]*WorkspaceGroup, 0) groups := make([]*WorkspaceGroup, 0)
for _, group := range m.groups { m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
if group.removed { if group.removed {
continue return true
} }
outputs := make([]string, 0) outputs := make([]string, 0)
for outputID := range group.outputIDs { for outputID := range group.outputIDs {
m.outputsMutex.RLock() if name, ok := m.outputNames.Load(outputID); ok && name != "" {
name := m.outputNames[outputID]
m.outputsMutex.RUnlock()
if name != "" {
outputs = append(outputs, name) outputs = append(outputs, name)
} else {
outputs = append(outputs, fmt.Sprintf("output-%d", outputID))
} }
} }
workspaces := make([]*Workspace, 0) workspaces := make([]*Workspace, 0)
for _, wsID := range group.workspaceIDs { for _, wsID := range group.workspaceIDs {
ws, exists := m.workspaces[wsID] ws, exists := m.workspaces.Load(wsID)
if !exists || ws.removed { if !exists {
continue
}
if ws.removed {
continue continue
} }
@@ -351,10 +360,8 @@ func (m *Manager) updateState() {
Workspaces: workspaces, Workspaces: workspaces,
} }
groups = append(groups, groupState) groups = append(groups, groupState)
} return true
})
m.workspacesMutex.RUnlock()
m.groupsMutex.RUnlock()
newState := State{ newState := State{
Groups: groups, Groups: groups,
@@ -389,14 +396,6 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
subCount := len(m.subscribers)
m.subMutex.RUnlock()
if subCount == 0 {
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
@@ -405,15 +404,14 @@ func (m *Manager) notifier() {
continue continue
} }
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
log.Warn("ExtWorkspace: subscriber channel full, dropping update") log.Warn("ExtWorkspace: subscriber channel full, dropping update")
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -426,9 +424,6 @@ func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error {
errChan := make(chan error, 1) errChan := make(chan error, 1)
m.post(func() { m.post(func() {
m.workspacesMutex.RLock()
defer m.workspacesMutex.RUnlock()
var targetGroupID uint32 var targetGroupID uint32
if groupID != "" { if groupID != "" {
var parsedID uint32 var parsedID uint32
@@ -437,9 +432,10 @@ func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error {
} }
} }
for _, ws := range m.workspaces { var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID { if targetGroupID != 0 && ws.groupID != targetGroupID {
continue return true
} }
if ws.workspaceID == workspaceID || ws.name == workspaceID { if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -449,11 +445,15 @@ func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error {
} }
m.wlMutex.Unlock() m.wlMutex.Unlock()
errChan <- err errChan <- err
return found = true
return false
} }
} return true
})
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
}) })
return <-errChan return <-errChan
@@ -463,9 +463,6 @@ func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error {
errChan := make(chan error, 1) errChan := make(chan error, 1)
m.post(func() { m.post(func() {
m.workspacesMutex.RLock()
defer m.workspacesMutex.RUnlock()
var targetGroupID uint32 var targetGroupID uint32
if groupID != "" { if groupID != "" {
var parsedID uint32 var parsedID uint32
@@ -474,9 +471,10 @@ func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error {
} }
} }
for _, ws := range m.workspaces { var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID { if targetGroupID != 0 && ws.groupID != targetGroupID {
continue return true
} }
if ws.workspaceID == workspaceID || ws.name == workspaceID { if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -486,11 +484,15 @@ func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error {
} }
m.wlMutex.Unlock() m.wlMutex.Unlock()
errChan <- err errChan <- err
return found = true
return false
} }
} return true
})
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
}) })
return <-errChan return <-errChan
@@ -500,9 +502,6 @@ func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error {
errChan := make(chan error, 1) errChan := make(chan error, 1)
m.post(func() { m.post(func() {
m.workspacesMutex.RLock()
defer m.workspacesMutex.RUnlock()
var targetGroupID uint32 var targetGroupID uint32
if groupID != "" { if groupID != "" {
var parsedID uint32 var parsedID uint32
@@ -511,9 +510,10 @@ func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error {
} }
} }
for _, ws := range m.workspaces { var found bool
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
if targetGroupID != 0 && ws.groupID != targetGroupID { if targetGroupID != 0 && ws.groupID != targetGroupID {
continue return true
} }
if ws.workspaceID == workspaceID || ws.name == workspaceID { if ws.workspaceID == workspaceID || ws.name == workspaceID {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -523,11 +523,15 @@ func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error {
} }
m.wlMutex.Unlock() m.wlMutex.Unlock()
errChan <- err errChan <- err
return found = true
return false
} }
} return true
})
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID) if !found {
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
}
}) })
return <-errChan return <-errChan
@@ -537,10 +541,8 @@ func (m *Manager) CreateWorkspace(groupID, workspaceName string) error {
errChan := make(chan error, 1) errChan := make(chan error, 1)
m.post(func() { m.post(func() {
m.groupsMutex.RLock() var found bool
defer m.groupsMutex.RUnlock() m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
for _, group := range m.groups {
if fmt.Sprintf("group-%d", group.id) == groupID { if fmt.Sprintf("group-%d", group.id) == groupID {
m.wlMutex.Lock() m.wlMutex.Lock()
err := group.handle.CreateWorkspace(workspaceName) err := group.handle.CreateWorkspace(workspaceName)
@@ -549,11 +551,15 @@ func (m *Manager) CreateWorkspace(groupID, workspaceName string) error {
} }
m.wlMutex.Unlock() m.wlMutex.Unlock()
errChan <- err errChan <- err
return found = true
return false
} }
} return true
})
errChan <- fmt.Errorf("workspace group not found: %s", groupID) if !found {
errChan <- fmt.Errorf("workspace group not found: %s", groupID)
}
}) })
return <-errChan return <-errChan
@@ -564,30 +570,27 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.workspacesMutex.Lock() m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
for _, ws := range m.workspaces {
if ws.handle != nil { if ws.handle != nil {
ws.handle.Destroy() ws.handle.Destroy()
} }
} m.workspaces.Delete(key)
m.workspaces = make(map[uint32]*workspaceState) return true
m.workspacesMutex.Unlock() })
m.groupsMutex.Lock() m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
for _, group := range m.groups {
if group.handle != nil { if group.handle != nil {
group.handle.Destroy() group.handle.Destroy()
} }
} m.groups.Delete(key)
m.groups = make(map[uint32]*workspaceGroupState) return true
m.groupsMutex.Unlock() })
if m.manager != nil { if m.manager != nil {
m.manager.Stop() m.manager.Stop()
+10 -17
View File
@@ -4,7 +4,8 @@ import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type Workspace struct { type Workspace struct {
@@ -37,23 +38,18 @@ type Manager struct {
registry *wlclient.Registry registry *wlclient.Registry
manager *ext_workspace.ExtWorkspaceManagerV1 manager *ext_workspace.ExtWorkspaceManagerV1
outputsMutex sync.RWMutex outputNames syncmap.Map[uint32, string]
outputs map[uint32]*wlclient.Output
outputNames map[uint32]string
groupsMutex sync.RWMutex groups syncmap.Map[uint32, *workspaceGroupState]
groups map[uint32]*workspaceGroupState
workspacesMutex sync.RWMutex workspaces syncmap.Map[uint32, *workspaceState]
workspaces map[uint32]*workspaceState
wlMutex sync.Mutex wlMutex sync.Mutex
cmdq chan cmd cmdq chan cmd
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -95,19 +91,16 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if ch, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok {
close(ch) close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {
+10 -20
View File
@@ -29,8 +29,6 @@ func NewManager() (*Manager, error) {
systemConn: systemConn, systemConn: systemConn,
sessionConn: sessionConn, sessionConn: sessionConn,
currentUID: uint64(os.Getuid()), currentUID: uint64(os.Getuid()),
subscribers: make(map[string]chan FreedeskState),
subMutex: sync.RWMutex{},
} }
m.initializeAccounts() m.initializeAccounts()
@@ -206,41 +204,33 @@ func (m *Manager) GetState() FreedeskState {
func (m *Manager) Subscribe(id string) chan FreedeskState { func (m *Manager) Subscribe(id string) chan FreedeskState {
ch := make(chan FreedeskState, 64) ch := make(chan FreedeskState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) NotifySubscribers() { func (m *Manager) NotifySubscribers() {
m.subMutex.RLock()
defer m.subMutex.RUnlock()
state := m.GetState() state := m.GetState()
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan FreedeskState) bool {
select { select {
case ch <- state: case ch <- state:
default: default:
} }
} return true
})
} }
func (m *Manager) Close() { func (m *Manager) Close() {
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan FreedeskState) bool {
for id, ch := range m.subscribers {
close(ch) close(ch)
delete(m.subscribers, id) m.subscribers.Delete(key)
} return true
m.subMutex.Unlock() })
if m.systemConn != nil { if m.systemConn != nil {
m.systemConn.Close() m.systemConn.Close()
+2 -2
View File
@@ -3,6 +3,7 @@ package freedesktop
import ( import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -41,6 +42,5 @@ type Manager struct {
accountsObj dbus.BusObject accountsObj dbus.BusObject
settingsObj dbus.BusObject settingsObj dbus.BusObject
currentUID uint64 currentUID uint64
subscribers map[string]chan FreedeskState subscribers syncmap.Map[string, chan FreedeskState]
subMutex sync.RWMutex
} }
@@ -466,9 +466,7 @@ func TestHandleSubscribe(t *testing.T) {
SessionID: "1", SessionID: "1",
Locked: false, Locked: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{},
} }
conn := newMockNetConn() conn := newMockNetConn()
+16 -30
View File
@@ -25,13 +25,12 @@ func NewManager() (*Manager, error) {
state: &SessionState{ state: &SessionState{
SessionID: sessionID, SessionID: sessionID,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{}, stopChan: make(chan struct{}),
stopChan: make(chan struct{}), conn: conn,
conn: conn, dirty: make(chan struct{}, 1),
dirty: make(chan struct{}, 1), signals: make(chan *dbus.Signal, 256),
signals: make(chan *dbus.Signal, 256),
} }
m.sleepInhibitorEnabled.Store(true) m.sleepInhibitorEnabled.Store(true)
@@ -351,19 +350,14 @@ func (m *Manager) GetState() SessionState {
func (m *Manager) Subscribe(id string) chan SessionState { func (m *Manager) Subscribe(id string) chan SessionState {
ch := make(chan SessionState, 64) ch := make(chan SessionState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifier() { func (m *Manager) notifier() {
@@ -387,28 +381,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan SessionState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -584,12 +571,11 @@ func (m *Manager) Close() {
m.releaseSleepInhibitor() m.releaseSleepInhibitor()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan SessionState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan SessionState) return true
m.subMutex.Unlock() })
if m.conn != nil { if m.conn != nil {
m.conn.Close() m.conn.Close()
+24 -41
View File
@@ -34,26 +34,20 @@ func TestManager_GetState(t *testing.T) {
func TestManager_Subscribe(t *testing.T) { func TestManager_Subscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{},
} }
ch := manager.Subscribe("test-client") ch := manager.Subscribe("test-client")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Equal(t, 64, cap(ch)) assert.Equal(t, 64, cap(ch))
manager.subMutex.RLock() _, exists := manager.subscribers.Load("test-client")
_, exists := manager.subscribers["test-client"]
manager.subMutex.RUnlock()
assert.True(t, exists) assert.True(t, exists)
} }
func TestManager_Unsubscribe(t *testing.T) { func TestManager_Unsubscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{},
} }
ch := manager.Subscribe("test-client") ch := manager.Subscribe("test-client")
@@ -63,17 +57,13 @@ func TestManager_Unsubscribe(t *testing.T) {
_, ok := <-ch _, ok := <-ch
assert.False(t, ok) assert.False(t, ok)
manager.subMutex.RLock() _, exists := manager.subscribers.Load("test-client")
_, exists := manager.subscribers["test-client"]
manager.subMutex.RUnlock()
assert.False(t, exists) assert.False(t, exists)
} }
func TestManager_Unsubscribe_NonExistent(t *testing.T) { func TestManager_Unsubscribe_NonExistent(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
subscribers: make(map[string]chan SessionState),
subMutex: sync.RWMutex{},
} }
// Unsubscribe a non-existent client should not panic // Unsubscribe a non-existent client should not panic
@@ -88,19 +78,15 @@ func TestManager_NotifySubscribers(t *testing.T) {
SessionID: "1", SessionID: "1",
Locked: false, Locked: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{}, dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
} }
manager.notifierWg.Add(1) manager.notifierWg.Add(1)
go manager.notifier() go manager.notifier()
ch := make(chan SessionState, 10) ch := make(chan SessionState, 10)
manager.subMutex.Lock() manager.subscribers.Store("test-client", ch)
manager.subscribers["test-client"] = ch
manager.subMutex.Unlock()
manager.notifySubscribers() manager.notifySubscribers()
@@ -122,19 +108,15 @@ func TestManager_NotifySubscribers_Debounce(t *testing.T) {
SessionID: "1", SessionID: "1",
Locked: false, Locked: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{}, dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
} }
manager.notifierWg.Add(1) manager.notifierWg.Add(1)
go manager.notifier() go manager.notifier()
ch := make(chan SessionState, 10) ch := make(chan SessionState, 10)
manager.subMutex.Lock() manager.subscribers.Store("test-client", ch)
manager.subscribers["test-client"] = ch
manager.subMutex.Unlock()
manager.notifySubscribers() manager.notifySubscribers()
manager.notifySubscribers() manager.notifySubscribers()
@@ -157,19 +139,15 @@ func TestManager_NotifySubscribers_Debounce(t *testing.T) {
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
} }
ch1 := make(chan SessionState, 1) ch1 := make(chan SessionState, 1)
ch2 := make(chan SessionState, 1) ch2 := make(chan SessionState, 1)
manager.subMutex.Lock() manager.subscribers.Store("client1", ch1)
manager.subscribers["client1"] = ch1 manager.subscribers.Store("client2", ch2)
manager.subscribers["client2"] = ch2
manager.subMutex.Unlock()
manager.Close() manager.Close()
@@ -184,7 +162,12 @@ func TestManager_Close(t *testing.T) {
assert.False(t, ok1, "ch1 should be closed") assert.False(t, ok1, "ch1 should be closed")
assert.False(t, ok2, "ch2 should be closed") assert.False(t, ok2, "ch2 should be closed")
assert.Len(t, manager.subscribers, 0) count := 0
manager.subscribers.Range(func(key string, ch chan SessionState) bool {
count++
return true
})
assert.Equal(t, 0, count)
} }
func TestManager_GetState_ThreadSafe(t *testing.T) { func TestManager_GetState_ThreadSafe(t *testing.T) {
+25 -49
View File
@@ -14,10 +14,8 @@ func TestManager_HandleDBusSignal_Lock(t *testing.T) {
Locked: false, Locked: false,
LockedHint: false, LockedHint: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -38,10 +36,8 @@ func TestManager_HandleDBusSignal_Unlock(t *testing.T) {
Locked: true, Locked: true,
LockedHint: true, LockedHint: true,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -62,10 +58,8 @@ func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) {
state: &SessionState{ state: &SessionState{
PreparingForSleep: false, PreparingForSleep: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -85,10 +79,8 @@ func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) {
state: &SessionState{ state: &SessionState{
PreparingForSleep: true, PreparingForSleep: true,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -108,10 +100,8 @@ func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) {
state: &SessionState{ state: &SessionState{
PreparingForSleep: false, PreparingForSleep: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -133,10 +123,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
state: &SessionState{ state: &SessionState{
Active: false, Active: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -161,10 +149,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
state: &SessionState{ state: &SessionState{
IdleHint: false, IdleHint: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -189,10 +175,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
state: &SessionState{ state: &SessionState{
IdleSinceHint: 0, IdleSinceHint: 0,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -218,10 +202,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
LockedHint: false, LockedHint: false,
Locked: false, Locked: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -247,10 +229,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
state: &SessionState{ state: &SessionState{
Active: false, Active: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -272,11 +252,9 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
t.Run("empty body", func(t *testing.T) { t.Run("empty body", func(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &SessionState{}, state: &SessionState{},
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
@@ -295,10 +273,8 @@ func TestManager_HandlePropertiesChanged(t *testing.T) {
Active: false, Active: false,
IdleHint: false, IdleHint: false,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan SessionState), dirty: make(chan struct{}, 1),
subMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1),
} }
sig := &dbus.Signal{ sig := &dbus.Signal{
+2 -2
View File
@@ -6,6 +6,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -50,8 +51,7 @@ type SessionEvent struct {
type Manager struct { type Manager struct {
state *SessionState state *SessionState
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan SessionState subscribers syncmap.Map[string, chan SessionState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
conn *dbus.Conn conn *dbus.Conn
sessionPath dbus.ObjectPath sessionPath dbus.ObjectPath
+10 -4
View File
@@ -240,19 +240,25 @@ func TestHandleSubscribe(t *testing.T) {
func TestManager_Subscribe_Unsubscribe(t *testing.T) { func TestManager_Subscribe_Unsubscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &NetworkState{}, state: &NetworkState{},
subscribers: make(map[string]chan NetworkState),
} }
t.Run("subscribe creates channel", func(t *testing.T) { t.Run("subscribe creates channel", func(t *testing.T) {
ch := manager.Subscribe("client1") ch := manager.Subscribe("client1")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Len(t, manager.subscribers, 1) count := 0
manager.subscribers.Range(func(key string, ch chan NetworkState) bool {
count++
return true
})
assert.Equal(t, 1, count)
}) })
t.Run("unsubscribe removes channel", func(t *testing.T) { t.Run("unsubscribe removes channel", func(t *testing.T) {
manager.Unsubscribe("client1") manager.Unsubscribe("client1")
assert.Len(t, manager.subscribers, 0) count := 0
manager.subscribers.Range(func(key string, ch chan NetworkState) bool { count++; return true })
assert.Equal(t, 0, count)
}) })
t.Run("unsubscribe non-existent client is safe", func(t *testing.T) { t.Run("unsubscribe non-existent client is safe", func(t *testing.T) {
+19 -42
View File
@@ -66,13 +66,10 @@ func NewManager() (*Manager, error) {
Preference: PreferenceAuto, Preference: PreferenceAuto,
WiFiNetworks: []WiFiNetwork{}, WiFiNetworks: []WiFiNetwork{},
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan NetworkState),
subMutex: sync.RWMutex{}, stopChan: make(chan struct{}),
stopChan: make(chan struct{}), dirty: make(chan struct{}, 1),
dirty: make(chan struct{}, 1),
credentialSubscribers: make(map[string]chan CredentialPrompt),
credSubMutex: sync.RWMutex{},
} }
broker := NewSubscriptionBroker(m.broadcastCredentialPrompt) broker := NewSubscriptionBroker(m.broadcastCredentialPrompt)
@@ -270,48 +267,36 @@ func (m *Manager) GetState() NetworkState {
func (m *Manager) Subscribe(id string) chan NetworkState { func (m *Manager) Subscribe(id string) chan NetworkState {
ch := make(chan NetworkState, 64) ch := make(chan NetworkState, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) SubscribeCredentials(id string) chan CredentialPrompt { func (m *Manager) SubscribeCredentials(id string) chan CredentialPrompt {
ch := make(chan CredentialPrompt, 16) ch := make(chan CredentialPrompt, 16)
m.credSubMutex.Lock() m.credentialSubscribers.Store(id, ch)
m.credentialSubscribers[id] = ch
m.credSubMutex.Unlock()
return ch return ch
} }
func (m *Manager) UnsubscribeCredentials(id string) { func (m *Manager) UnsubscribeCredentials(id string) {
m.credSubMutex.Lock() if ch, ok := m.credentialSubscribers.LoadAndDelete(id); ok {
if ch, ok := m.credentialSubscribers[id]; ok {
close(ch) close(ch)
delete(m.credentialSubscribers, id)
} }
m.credSubMutex.Unlock()
} }
func (m *Manager) broadcastCredentialPrompt(prompt CredentialPrompt) { func (m *Manager) broadcastCredentialPrompt(prompt CredentialPrompt) {
m.credSubMutex.RLock() m.credentialSubscribers.Range(func(key string, ch chan CredentialPrompt) bool {
defer m.credSubMutex.RUnlock()
for _, ch := range m.credentialSubscribers {
select { select {
case ch <- prompt: case ch <- prompt:
default: default:
} }
} return true
})
} }
func (m *Manager) notifier() { func (m *Manager) notifier() {
@@ -335,28 +320,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.snapshotState() currentState := m.snapshotState()
if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, &currentState) { if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan NetworkState) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotifiedState = &stateCopy m.lastNotifiedState = &stateCopy
@@ -396,12 +374,11 @@ func (m *Manager) Close() {
m.backend.Close() m.backend.Close()
} }
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan NetworkState) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan NetworkState) return true
m.subMutex.Unlock() })
} }
func (m *Manager) ScanWiFi() error { func (m *Manager) ScanWiFi() error {
+20 -38
View File
@@ -31,19 +31,15 @@ func TestManager_NotifySubscribers(t *testing.T) {
state: &NetworkState{ state: &NetworkState{
NetworkStatus: StatusWiFi, NetworkStatus: StatusWiFi,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan NetworkState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{}, dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
} }
manager.notifierWg.Add(1) manager.notifierWg.Add(1)
go manager.notifier() go manager.notifier()
ch := make(chan NetworkState, 10) ch := make(chan NetworkState, 10)
manager.subMutex.Lock() manager.subscribers.Store("test-client", ch)
manager.subscribers["test-client"] = ch
manager.subMutex.Unlock()
manager.notifySubscribers() manager.notifySubscribers()
@@ -63,19 +59,15 @@ func TestManager_NotifySubscribers_Debounce(t *testing.T) {
state: &NetworkState{ state: &NetworkState{
NetworkStatus: StatusWiFi, NetworkStatus: StatusWiFi,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan NetworkState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{}, dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
} }
manager.notifierWg.Add(1) manager.notifierWg.Add(1)
go manager.notifier() go manager.notifier()
ch := make(chan NetworkState, 10) ch := make(chan NetworkState, 10)
manager.subMutex.Lock() manager.subscribers.Store("test-client", ch)
manager.subscribers["test-client"] = ch
manager.subMutex.Unlock()
manager.notifySubscribers() manager.notifySubscribers()
manager.notifySubscribers() manager.notifySubscribers()
@@ -98,19 +90,15 @@ func TestManager_NotifySubscribers_Debounce(t *testing.T) {
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &NetworkState{}, state: &NetworkState{},
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
subscribers: make(map[string]chan NetworkState), stopChan: make(chan struct{}),
subMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
} }
ch1 := make(chan NetworkState, 1) ch1 := make(chan NetworkState, 1)
ch2 := make(chan NetworkState, 1) ch2 := make(chan NetworkState, 1)
manager.subMutex.Lock() manager.subscribers.Store("client1", ch1)
manager.subscribers["client1"] = ch1 manager.subscribers.Store("client2", ch2)
manager.subscribers["client2"] = ch2
manager.subMutex.Unlock()
manager.Close() manager.Close()
@@ -125,31 +113,27 @@ func TestManager_Close(t *testing.T) {
assert.False(t, ok1, "ch1 should be closed") assert.False(t, ok1, "ch1 should be closed")
assert.False(t, ok2, "ch2 should be closed") assert.False(t, ok2, "ch2 should be closed")
assert.Len(t, manager.subscribers, 0) count := 0
manager.subscribers.Range(func(key string, ch chan NetworkState) bool { count++; return true })
assert.Equal(t, 0, count)
} }
func TestManager_Subscribe(t *testing.T) { func TestManager_Subscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &NetworkState{}, state: &NetworkState{},
subscribers: make(map[string]chan NetworkState),
subMutex: sync.RWMutex{},
} }
ch := manager.Subscribe("test-client") ch := manager.Subscribe("test-client")
assert.NotNil(t, ch) assert.NotNil(t, ch)
assert.Equal(t, 64, cap(ch)) assert.Equal(t, 64, cap(ch))
manager.subMutex.RLock() _, exists := manager.subscribers.Load("test-client")
_, exists := manager.subscribers["test-client"]
manager.subMutex.RUnlock()
assert.True(t, exists) assert.True(t, exists)
} }
func TestManager_Unsubscribe(t *testing.T) { func TestManager_Unsubscribe(t *testing.T) {
manager := &Manager{ manager := &Manager{
state: &NetworkState{}, state: &NetworkState{},
subscribers: make(map[string]chan NetworkState),
subMutex: sync.RWMutex{},
} }
ch := manager.Subscribe("test-client") ch := manager.Subscribe("test-client")
@@ -159,9 +143,7 @@ func TestManager_Unsubscribe(t *testing.T) {
_, ok := <-ch _, ok := <-ch
assert.False(t, ok) assert.False(t, ok)
manager.subMutex.RLock() _, exists := manager.subscribers.Load("test-client")
_, exists := manager.subscribers["test-client"]
manager.subMutex.RUnlock()
assert.False(t, exists) assert.False(t, exists)
} }
@@ -3,37 +3,29 @@ package network
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type SubscriptionBroker struct { type SubscriptionBroker struct {
mu sync.RWMutex pending syncmap.Map[string, chan PromptReply]
pending map[string]chan PromptReply requests syncmap.Map[string, PromptRequest]
requests map[string]PromptRequest pathSettingToToken syncmap.Map[string, string]
pathSettingToToken map[string]string
broadcastPrompt func(CredentialPrompt) broadcastPrompt func(CredentialPrompt)
} }
func NewSubscriptionBroker(broadcastPrompt func(CredentialPrompt)) PromptBroker { func NewSubscriptionBroker(broadcastPrompt func(CredentialPrompt)) PromptBroker {
return &SubscriptionBroker{ return &SubscriptionBroker{
pending: make(map[string]chan PromptReply), broadcastPrompt: broadcastPrompt,
requests: make(map[string]PromptRequest),
pathSettingToToken: make(map[string]string),
broadcastPrompt: broadcastPrompt,
} }
} }
func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string, error) { func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string, error) {
pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName) pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName)
b.mu.Lock() if existingToken, alreadyPending := b.pathSettingToToken.Load(pathSettingKey); alreadyPending {
existingToken, alreadyPending := b.pathSettingToToken[pathSettingKey]
b.mu.Unlock()
if alreadyPending {
log.Infof("[SubscriptionBroker] Duplicate prompt for %s, returning existing token", pathSettingKey) log.Infof("[SubscriptionBroker] Duplicate prompt for %s, returning existing token", pathSettingKey)
return existingToken, nil return existingToken, nil
} }
@@ -44,11 +36,9 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
replyChan := make(chan PromptReply, 1) replyChan := make(chan PromptReply, 1)
b.mu.Lock() b.pending.Store(token, replyChan)
b.pending[token] = replyChan b.requests.Store(token, req)
b.requests[token] = req b.pathSettingToToken.Store(pathSettingKey, token)
b.pathSettingToToken[pathSettingKey] = token
b.mu.Unlock()
if b.broadcastPrompt != nil { if b.broadcastPrompt != nil {
prompt := CredentialPrompt{ prompt := CredentialPrompt{
@@ -71,10 +61,7 @@ func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string
} }
func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) { func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
return PromptReply{}, fmt.Errorf("unknown token: %s", token) return PromptReply{}, fmt.Errorf("unknown token: %s", token)
} }
@@ -93,10 +80,7 @@ func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptRepl
} }
func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error { func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
b.mu.RLock() replyChan, exists := b.pending.Load(token)
replyChan, exists := b.pending[token]
b.mu.RUnlock()
if !exists { if !exists {
log.Warnf("[SubscriptionBroker] Resolve: unknown or expired token: %s", token) log.Warnf("[SubscriptionBroker] Resolve: unknown or expired token: %s", token)
return fmt.Errorf("unknown or expired token: %s", token) return fmt.Errorf("unknown or expired token: %s", token)
@@ -112,25 +96,19 @@ func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
} }
func (b *SubscriptionBroker) cleanup(token string) { func (b *SubscriptionBroker) cleanup(token string) {
b.mu.Lock() if req, exists := b.requests.Load(token); exists {
defer b.mu.Unlock()
if req, exists := b.requests[token]; exists {
pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName) pathSettingKey := fmt.Sprintf("%s:%s", req.ConnectionPath, req.SettingName)
delete(b.pathSettingToToken, pathSettingKey) b.pathSettingToToken.Delete(pathSettingKey)
} }
delete(b.pending, token) b.pending.Delete(token)
delete(b.requests, token) b.requests.Delete(token)
} }
func (b *SubscriptionBroker) Cancel(path string, setting string) error { func (b *SubscriptionBroker) Cancel(path string, setting string) error {
pathSettingKey := fmt.Sprintf("%s:%s", path, setting) pathSettingKey := fmt.Sprintf("%s:%s", path, setting)
b.mu.Lock() token, exists := b.pathSettingToToken.Load(pathSettingKey)
token, exists := b.pathSettingToToken[pathSettingKey]
b.mu.Unlock()
if !exists { if !exists {
log.Infof("[SubscriptionBroker] Cancel: no pending prompt for %s", pathSettingKey) log.Infof("[SubscriptionBroker] Cancel: no pending prompt for %s", pathSettingKey)
return nil return nil
+4 -5
View File
@@ -6,10 +6,9 @@ func NewTestManager(backend Backend, state *NetworkState) *Manager {
state = &NetworkState{} state = &NetworkState{}
} }
return &Manager{ return &Manager{
backend: backend, backend: backend,
state: state, state: state,
subscribers: make(map[string]chan NetworkState), stopChan: make(chan struct{}),
stopChan: make(chan struct{}), dirty: make(chan struct{}, 1),
dirty: make(chan struct{}, 1),
} }
} }
+3 -4
View File
@@ -3,6 +3,7 @@ package network
import ( import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -108,14 +109,12 @@ type Manager struct {
backend Backend backend Backend
state *NetworkState state *NetworkState
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers map[string]chan NetworkState subscribers syncmap.Map[string, chan NetworkState]
subMutex sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotifiedState *NetworkState lastNotifiedState *NetworkState
credentialSubscribers map[string]chan CredentialPrompt credentialSubscribers syncmap.Map[string, chan CredentialPrompt]
credSubMutex sync.RWMutex
} }
type EventType string type EventType string
+14 -2
View File
@@ -140,8 +140,20 @@ func RouteRequest(conn net.Conn, req models.Request) {
if strings.HasPrefix(req.Method, "extworkspace.") { if strings.HasPrefix(req.Method, "extworkspace.") {
if extWorkspaceManager == nil { if extWorkspaceManager == nil {
models.RespondError(conn, req.ID, "extworkspace manager not initialized") if extWorkspaceAvailable.Load() {
return extWorkspaceInitMutex.Lock()
if extWorkspaceManager == nil {
if err := InitializeExtWorkspaceManager(); err != nil {
extWorkspaceInitMutex.Unlock()
models.RespondError(conn, req.ID, "extworkspace manager not available")
return
}
}
extWorkspaceInitMutex.Unlock()
} else {
models.RespondError(conn, req.ID, "extworkspace manager not initialized")
return
}
} }
extWorkspaceReq := extworkspace.Request{ extWorkspaceReq := extworkspace.Request{
ID: req.ID, ID: req.ID,
+71 -59
View File
@@ -10,6 +10,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"time" "time"
@@ -27,9 +28,10 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
const APIVersion = 18 const APIVersion = 19
type Capabilities struct { type Capabilities struct {
Capabilities []string `json:"capabilities"` Capabilities []string `json:"capabilities"`
@@ -58,11 +60,11 @@ var wlrOutputManager *wlroutput.Manager
var evdevManager *evdev.Manager var evdevManager *evdev.Manager
var wlContext *wlcontext.SharedContext var wlContext *wlcontext.SharedContext
var capabilitySubscribers = make(map[string]chan ServerInfo) var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
var capabilityMutex sync.RWMutex var cupsSubscribers syncmap.Map[string, bool]
var cupsSubscriberCount atomic.Int32
var cupsSubscribers = make(map[string]bool) var extWorkspaceAvailable atomic.Bool
var cupsSubscribersMutex sync.Mutex var extWorkspaceInitMutex sync.Mutex
func getSocketDir() string { func getSocketDir() string {
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" { if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
@@ -361,7 +363,7 @@ func getCapabilities() Capabilities {
caps = append(caps, "dwl") caps = append(caps, "dwl")
} }
if extWorkspaceManager != nil { if extWorkspaceAvailable.Load() {
caps = append(caps, "extworkspace") caps = append(caps, "extworkspace")
} }
@@ -411,7 +413,7 @@ func getServerInfo() ServerInfo {
caps = append(caps, "dwl") caps = append(caps, "dwl")
} }
if extWorkspaceManager != nil { if extWorkspaceAvailable.Load() {
caps = append(caps, "extworkspace") caps = append(caps, "extworkspace")
} }
@@ -434,16 +436,14 @@ func getServerInfo() ServerInfo {
} }
func notifyCapabilityChange() { func notifyCapabilityChange() {
capabilityMutex.RLock()
defer capabilityMutex.RUnlock()
info := getServerInfo() info := getServerInfo()
for _, ch := range capabilitySubscribers { capabilitySubscribers.Range(func(key string, ch chan ServerInfo) bool {
select { select {
case ch <- info: case ch <- info:
default: default:
} }
} return true
})
} }
func handleSubscribe(conn net.Conn, req models.Request) { func handleSubscribe(conn net.Conn, req models.Request) {
@@ -475,18 +475,12 @@ func handleSubscribe(conn net.Conn, req models.Request) {
stopChan := make(chan struct{}) stopChan := make(chan struct{})
capChan := make(chan ServerInfo, 64) capChan := make(chan ServerInfo, 64)
capabilityMutex.Lock() capabilitySubscribers.Store(clientID+"-capabilities", capChan)
capabilitySubscribers[clientID+"-capabilities"] = capChan
capabilityMutex.Unlock()
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
defer func() { defer capabilitySubscribers.Delete(clientID + "-capabilities")
capabilityMutex.Lock()
delete(capabilitySubscribers, clientID+"-capabilities")
capabilityMutex.Unlock()
}()
for { for {
select { select {
@@ -728,12 +722,10 @@ func handleSubscribe(conn net.Conn, req models.Request) {
} }
if shouldSubscribe("cups") { if shouldSubscribe("cups") {
cupsSubscribersMutex.Lock() cupsSubscribers.Store(clientID+"-cups", true)
wasEmpty := len(cupsSubscribers) == 0 count := cupsSubscriberCount.Add(1)
cupsSubscribers[clientID+"-cups"] = true
cupsSubscribersMutex.Unlock()
if wasEmpty { if count == 1 {
if err := InitializeCupsManager(); err != nil { if err := InitializeCupsManager(); err != nil {
log.Warnf("Failed to initialize CUPS manager for subscription: %v", err) log.Warnf("Failed to initialize CUPS manager for subscription: %v", err)
} else { } else {
@@ -748,13 +740,10 @@ func handleSubscribe(conn net.Conn, req models.Request) {
defer wg.Done() defer wg.Done()
defer func() { defer func() {
cupsManager.Unsubscribe(clientID + "-cups") cupsManager.Unsubscribe(clientID + "-cups")
cupsSubscribers.Delete(clientID + "-cups")
count := cupsSubscriberCount.Add(-1)
cupsSubscribersMutex.Lock() if count == 0 {
delete(cupsSubscribers, clientID+"-cups")
isEmpty := len(cupsSubscribers) == 0
cupsSubscribersMutex.Unlock()
if isEmpty {
log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager") log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager")
if cupsManager != nil { if cupsManager != nil {
cupsManager.Close() cupsManager.Close()
@@ -822,36 +811,48 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}() }()
} }
if shouldSubscribe("extworkspace") && extWorkspaceManager != nil { if shouldSubscribe("extworkspace") {
wg.Add(1) if extWorkspaceManager == nil && extWorkspaceAvailable.Load() {
extWorkspaceChan := extWorkspaceManager.Subscribe(clientID + "-extworkspace") extWorkspaceInitMutex.Lock()
go func() { if extWorkspaceManager == nil {
defer wg.Done() if err := InitializeExtWorkspaceManager(); err != nil {
defer extWorkspaceManager.Unsubscribe(clientID + "-extworkspace") log.Warnf("Failed to initialize ExtWorkspace manager for subscription: %v", err)
}
initialState := extWorkspaceManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "extworkspace", Data: initialState}:
case <-stopChan:
return
} }
extWorkspaceInitMutex.Unlock()
}
for { if extWorkspaceManager != nil {
wg.Add(1)
extWorkspaceChan := extWorkspaceManager.Subscribe(clientID + "-extworkspace")
go func() {
defer wg.Done()
defer extWorkspaceManager.Unsubscribe(clientID + "-extworkspace")
initialState := extWorkspaceManager.GetState()
select { select {
case state, ok := <-extWorkspaceChan: case eventChan <- ServiceEvent{Service: "extworkspace", Data: initialState}:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "extworkspace", Data: state}:
case <-stopChan:
return
}
case <-stopChan: case <-stopChan:
return return
} }
}
}() for {
select {
case state, ok := <-extWorkspaceChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "extworkspace", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
} }
if shouldSubscribe("brightness") && brightnessManager != nil { if shouldSubscribe("brightness") && brightnessManager != nil {
@@ -1144,11 +1145,18 @@ func Start(printDocs bool) error {
log.Info(" cups.cancelJob - Cancel job (params: printerName, jobID)") log.Info(" cups.cancelJob - Cancel job (params: printerName, jobID)")
log.Info(" cups.purgeJobs - Cancel all jobs (params: printerName)") log.Info(" cups.purgeJobs - Cancel all jobs (params: printerName)")
log.Info("DWL:") log.Info("DWL:")
log.Info(" dwl.getState - Get current dwl state (tags, windows, layouts)") log.Info(" dwl.getState - Get current dwl state (tags, windows, layouts, keyboard)")
log.Info(" dwl.setTags - Set active tags (params: output, tagmask, toggleTagset)") log.Info(" dwl.setTags - Set active tags (params: output, tagmask, toggleTagset)")
log.Info(" dwl.setClientTags - Set focused client tags (params: output, andTags, xorTags)") log.Info(" dwl.setClientTags - Set focused client tags (params: output, andTags, xorTags)")
log.Info(" dwl.setLayout - Set layout (params: output, index)") log.Info(" dwl.setLayout - Set layout (params: output, index)")
log.Info(" dwl.subscribe - Subscribe to dwl state changes (streaming)") log.Info(" dwl.subscribe - Subscribe to dwl state changes (streaming)")
log.Info(" Output state includes:")
log.Info(" - tags : Tag states (active, clients, focused)")
log.Info(" - layoutSymbol : Current layout name")
log.Info(" - title : Focused window title")
log.Info(" - appId : Focused window app ID")
log.Info(" - kbLayout : Current keyboard layout")
log.Info(" - keymode : Current keybind mode")
log.Info("ExtWorkspace:") log.Info("ExtWorkspace:")
log.Info(" extworkspace.getState - Get current workspace state (groups, workspaces)") log.Info(" extworkspace.getState - Get current workspace state (groups, workspaces)")
log.Info(" extworkspace.activateWorkspace - Activate workspace (params: groupID, workspaceID)") log.Info(" extworkspace.activateWorkspace - Activate workspace (params: groupID, workspaceID)")
@@ -1244,8 +1252,12 @@ func Start(printDocs bool) error {
log.Debugf("DWL manager unavailable: %v", err) log.Debugf("DWL manager unavailable: %v", err)
} }
if err := InitializeExtWorkspaceManager(); err != nil { if extworkspace.CheckCapability() {
log.Debugf("ExtWorkspace manager unavailable: %v", err) extWorkspaceAvailable.Store(true)
log.Info("ExtWorkspace capability detected and will be available on subscription")
} else {
log.Debug("ExtWorkspace capability not available")
extWorkspaceAvailable.Store(false)
} }
if err := InitializeWlrOutputManager(); err != nil { if err := InitializeWlrOutputManager(); err != nil {
+98 -118
View File
@@ -8,8 +8,8 @@ import (
"syscall" "syscall"
"time" "time"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
wlclient "github.com/yaslama/go-wayland/wayland/client"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
@@ -23,14 +23,13 @@ func NewManager(display *wlclient.Display, config Config) (*Manager, error) {
} }
m := &Manager{ m := &Manager{
config: config, config: config,
display: display, display: display,
ctx: display.Context(), ctx: display.Context(),
outputs: make(map[uint32]*outputState), cmdq: make(chan cmd, 128),
cmdq: make(chan cmd, 128), stopChan: make(chan struct{}),
stopChan: make(chan struct{}), updateTrigger: make(chan struct{}, 1),
updateTrigger: make(chan struct{}, 1),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
dbusSignal: make(chan *dbus.Signal, 16), dbusSignal: make(chan *dbus.Signal, 16),
transitionChan: make(chan int, 1), transitionChan: make(chan int, 1),
@@ -114,17 +113,17 @@ func (m *Manager) waylandActor() {
} }
func (m *Manager) allOutputsReady() bool { func (m *Manager) allOutputsReady() bool {
m.outputsMutex.RLock() hasOutputs := false
defer m.outputsMutex.RUnlock() allReady := true
if len(m.outputs) == 0 { m.outputs.Range(func(key uint32, value *outputState) bool {
return false hasOutputs = true
} if value.rampSize == 0 || value.failed {
for _, o := range m.outputs { allReady = false
if o.rampSize == 0 || o.failed {
return false return false
} }
} return true
return true })
return hasOutputs && allReady
} }
func (m *Manager) setupDBusMonitor() error { func (m *Manager) setupDBusMonitor() error {
@@ -157,7 +156,6 @@ func (m *Manager) setupRegistry() error {
m.registry = registry m.registry = registry
outputs := make([]*wlclient.Output, 0) outputs := make([]*wlclient.Output, 0)
outputRegNames := make(map[uint32]uint32)
outputNames := make(map[uint32]string) outputNames := make(map[uint32]string)
var gammaMgr *wlr_gamma_control.ZwlrGammaControlManagerV1 var gammaMgr *wlr_gamma_control.ZwlrGammaControlManagerV1
@@ -198,14 +196,9 @@ func (m *Manager) setupRegistry() error {
if gammaMgr != nil { if gammaMgr != nil {
outputs = append(outputs, output) outputs = append(outputs, output)
outputRegNames[outputID] = e.Name
} }
m.outputsMutex.Lock() m.outputRegNames.Store(outputID, e.Name)
if m.outputRegNames != nil {
m.outputRegNames[outputID] = e.Name
}
m.outputsMutex.Unlock()
m.configMutex.RLock() m.configMutex.RLock()
enabled := m.config.Enabled enabled := m.config.Enabled
@@ -236,23 +229,33 @@ func (m *Manager) setupRegistry() error {
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) { registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
m.post(func() { m.post(func() {
m.outputsMutex.Lock() var foundID uint32
defer m.outputsMutex.Unlock() var foundOut *outputState
m.outputs.Range(func(id uint32, out *outputState) bool {
for id, out := range m.outputs {
if out.registryName == e.Name { if out.registryName == e.Name {
log.Infof("Output %d (registry name %d) removed, destroying gamma control", id, e.Name) foundID = id
if out.gammaControl != nil { foundOut = out
control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) return false
control.Destroy() }
} return true
delete(m.outputs, id) })
if len(m.outputs) == 0 { if foundOut != nil {
m.controlsInitialized = false log.Infof("Output %d (registry name %d) removed, destroying gamma control", foundID, e.Name)
log.Info("All outputs removed, controls no longer initialized") if foundOut.gammaControl != nil {
} control := foundOut.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
return control.Destroy()
}
m.outputs.Delete(foundID)
hasOutputs := false
m.outputs.Range(func(key uint32, value *outputState) bool {
hasOutputs = true
return false
})
if !hasOutputs {
m.controlsInitialized = false
log.Info("All outputs removed, controls no longer initialized")
} }
} }
}) })
@@ -292,7 +295,6 @@ func (m *Manager) setupRegistry() error {
m.gammaControl = gammaMgr m.gammaControl = gammaMgr
m.availableOutputs = physicalOutputs m.availableOutputs = physicalOutputs
m.outputRegNames = outputRegNames
log.Info("setupRegistry: completed successfully (gamma controls will be initialized when enabled)") log.Info("setupRegistry: completed successfully (gamma controls will be initialized when enabled)")
return nil return nil
@@ -308,9 +310,12 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
continue continue
} }
outputID := output.ID()
registryName, _ := m.outputRegNames.Load(outputID)
outState := &outputState{ outState := &outputState{
id: output.ID(), id: outputID,
registryName: m.outputRegNames[output.ID()], registryName: registryName,
output: output, output: output,
gammaControl: control, gammaControl: control,
isVirtual: false, isVirtual: false,
@@ -318,14 +323,12 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
func(state *outputState) { func(state *outputState) {
control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) {
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(state.id); exists {
if outState, exists := m.outputs[state.id]; exists {
outState.rampSize = e.Size outState.rampSize = e.Size
outState.failed = false outState.failed = false
outState.retryCount = 0 outState.retryCount = 0
log.Infof("Output %d gamma_size=%d", state.id, e.Size) log.Infof("Output %d gamma_size=%d", state.id, e.Size)
} }
m.outputsMutex.Unlock()
m.transitionMutex.RLock() m.transitionMutex.RLock()
currentTemp := m.currentTemp currentTemp := m.currentTemp
@@ -337,8 +340,7 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
}) })
control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) {
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(state.id); exists {
if outState, exists := m.outputs[state.id]; exists {
outState.failed = true outState.failed = true
outState.rampSize = 0 outState.rampSize = 0
outState.retryCount++ outState.retryCount++
@@ -357,13 +359,10 @@ func (m *Manager) setupOutputControls(outputs []*wlclient.Output, manager *wlr_g
}) })
}) })
} }
m.outputsMutex.Unlock()
}) })
}(outState) }(outState)
m.outputsMutex.Lock() m.outputs.Store(outputID, outState)
m.outputs[output.ID()] = outState
m.outputsMutex.Unlock()
} }
return nil return nil
@@ -375,8 +374,7 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
var outputName string var outputName string
output.SetNameHandler(func(ev wlclient.OutputNameEvent) { output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
outputName = ev.Name outputName = ev.Name
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(outputID); exists {
if outState, exists := m.outputs[outputID]; exists {
outState.name = ev.Name outState.name = ev.Name
if len(ev.Name) >= 9 && ev.Name[:9] == "HEADLESS-" { if len(ev.Name) >= 9 && ev.Name[:9] == "HEADLESS-" {
log.Infof("Detected virtual output %d (name=%s), marking for gamma control skip", outputID, ev.Name) log.Infof("Detected virtual output %d (name=%s), marking for gamma control skip", outputID, ev.Name)
@@ -384,7 +382,6 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
outState.failed = true outState.failed = true
} }
} }
m.outputsMutex.Unlock()
}) })
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
@@ -394,24 +391,24 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
return fmt.Errorf("failed to get gamma control: %w", err) return fmt.Errorf("failed to get gamma control: %w", err)
} }
registryName, _ := m.outputRegNames.Load(outputID)
outState := &outputState{ outState := &outputState{
id: outputID, id: outputID,
name: outputName, name: outputName,
registryName: m.outputRegNames[outputID], registryName: registryName,
output: output, output: output,
gammaControl: control, gammaControl: control,
isVirtual: false, isVirtual: false,
} }
control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) {
m.outputsMutex.Lock() if out, exists := m.outputs.Load(outState.id); exists {
if out, exists := m.outputs[outState.id]; exists {
out.rampSize = e.Size out.rampSize = e.Size
out.failed = false out.failed = false
out.retryCount = 0 out.retryCount = 0
log.Infof("Output %d gamma_size=%d", outState.id, e.Size) log.Infof("Output %d gamma_size=%d", outState.id, e.Size)
} }
m.outputsMutex.Unlock()
m.transitionMutex.RLock() m.transitionMutex.RLock()
currentTemp := m.currentTemp currentTemp := m.currentTemp
@@ -423,8 +420,7 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
}) })
control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) {
m.outputsMutex.Lock() if out, exists := m.outputs.Load(outState.id); exists {
if out, exists := m.outputs[outState.id]; exists {
out.failed = true out.failed = true
out.rampSize = 0 out.rampSize = 0
out.retryCount++ out.retryCount++
@@ -443,12 +439,9 @@ func (m *Manager) addOutputControl(output *wlclient.Output) error {
}) })
}) })
} }
m.outputsMutex.Unlock()
}) })
m.outputsMutex.Lock() m.outputs.Store(outputID, outState)
m.outputs[output.ID()] = outState
m.outputsMutex.Unlock()
log.Infof("Added gamma control for output %d", output.ID()) log.Infof("Added gamma control for output %d", output.ID())
return nil return nil
@@ -623,17 +616,19 @@ func (m *Manager) transitionWorker() {
if !enabled && targetTemp == identityTemp && m.controlsInitialized { if !enabled && targetTemp == identityTemp && m.controlsInitialized {
m.post(func() { m.post(func() {
log.Info("Destroying gamma controls after transition to identity") log.Info("Destroying gamma controls after transition to identity")
m.outputsMutex.Lock() m.outputs.Range(func(id uint32, out *outputState) bool {
for id, out := range m.outputs {
if out.gammaControl != nil { if out.gammaControl != nil {
control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
control.Destroy() control.Destroy()
log.Debugf("Destroyed gamma control for output %d", id) log.Debugf("Destroyed gamma control for output %d", id)
} }
} return true
m.outputs = make(map[uint32]*outputState) })
m.outputs.Range(func(key uint32, value *outputState) bool {
m.outputs.Delete(key)
return true
})
m.controlsInitialized = false m.controlsInitialized = false
m.outputsMutex.Unlock()
m.transitionMutex.Lock() m.transitionMutex.Lock()
m.currentTemp = identityTemp m.currentTemp = identityTemp
@@ -661,9 +656,7 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
return nil return nil
} }
m.outputsMutex.RLock() _, exists := m.outputs.Load(out.id)
_, exists := m.outputs[out.id]
m.outputsMutex.RUnlock()
if !exists { if !exists {
return nil return nil
@@ -689,14 +682,12 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
state := out state := out
control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) { control.SetGammaSizeHandler(func(e wlr_gamma_control.ZwlrGammaControlV1GammaSizeEvent) {
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(state.id); exists {
if outState, exists := m.outputs[state.id]; exists {
outState.rampSize = e.Size outState.rampSize = e.Size
outState.failed = false outState.failed = false
outState.retryCount = 0 outState.retryCount = 0
log.Infof("Output %d gamma_size=%d (recreated)", state.id, e.Size) log.Infof("Output %d gamma_size=%d (recreated)", state.id, e.Size)
} }
m.outputsMutex.Unlock()
m.transitionMutex.RLock() m.transitionMutex.RLock()
currentTemp := m.currentTemp currentTemp := m.currentTemp
@@ -708,8 +699,7 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
}) })
control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) { control.SetFailedHandler(func(e wlr_gamma_control.ZwlrGammaControlV1FailedEvent) {
m.outputsMutex.Lock() if outState, exists := m.outputs.Load(state.id); exists {
if outState, exists := m.outputs[state.id]; exists {
outState.failed = true outState.failed = true
outState.rampSize = 0 outState.rampSize = 0
outState.retryCount++ outState.retryCount++
@@ -728,7 +718,6 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
}) })
}) })
} }
m.outputsMutex.Unlock()
}) })
out.gammaControl = control out.gammaControl = control
@@ -750,13 +739,11 @@ func (m *Manager) applyNowOnActor(temp int) {
return return
} }
// Lock while snapshotting outputs to prevent races with recreateOutputControl
m.outputsMutex.RLock()
var outs []*outputState var outs []*outputState
for _, out := range m.outputs { m.outputs.Range(func(key uint32, value *outputState) bool {
outs = append(outs, out) outs = append(outs, value)
} return true
m.outputsMutex.RUnlock() })
if len(outs) == 0 { if len(outs) == 0 {
return return
@@ -796,20 +783,17 @@ func (m *Manager) applyNowOnActor(temp int) {
if err := m.setGammaBytesActor(j.out, j.data); err != nil { if err := m.setGammaBytesActor(j.out, j.data); err != nil {
log.Warnf("Failed to set gamma for output %d: %v", j.out.id, err) log.Warnf("Failed to set gamma for output %d: %v", j.out.id, err)
outID := j.out.id outID := j.out.id
m.outputsMutex.Lock() if out, exists := m.outputs.Load(outID); exists {
if out, exists := m.outputs[outID]; exists {
out.failed = true out.failed = true
out.rampSize = 0 out.rampSize = 0
} }
m.outputsMutex.Unlock()
time.AfterFunc(300*time.Millisecond, func() { time.AfterFunc(300*time.Millisecond, func() {
m.post(func() { m.post(func() {
m.outputsMutex.RLock() if out, exists := m.outputs.Load(outID); exists {
out, exists := m.outputs[outID] if out.failed {
m.outputsMutex.RUnlock() m.recreateOutputControl(out)
if exists && out.failed { }
m.recreateOutputControl(out)
} }
}) })
}) })
@@ -935,28 +919,21 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
if len(m.subscribers) == 0 {
m.subMutex.RUnlock()
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
if m.lastNotified != nil && !stateChanged(m.lastNotified, &currentState) { if m.lastNotified != nil && !stateChanged(m.lastNotified, &currentState) {
m.subMutex.RUnlock()
pending = false pending = false
continue continue
} }
for _, ch := range m.subscribers { m.subscribers.Range(func(key string, ch chan State) bool {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -1296,17 +1273,19 @@ func (m *Manager) SetEnabled(enabled bool) {
if currentTemp == identityTemp { if currentTemp == identityTemp {
m.post(func() { m.post(func() {
log.Infof("Already at %dK, destroying gamma controls immediately", identityTemp) log.Infof("Already at %dK, destroying gamma controls immediately", identityTemp)
m.outputsMutex.Lock() m.outputs.Range(func(id uint32, out *outputState) bool {
for id, out := range m.outputs {
if out.gammaControl != nil { if out.gammaControl != nil {
control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1) control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
control.Destroy() control.Destroy()
log.Debugf("Destroyed gamma control for output %d", id) log.Debugf("Destroyed gamma control for output %d", id)
} }
} return true
m.outputs = make(map[uint32]*outputState) })
m.outputs.Range(func(key uint32, value *outputState) bool {
m.outputs.Delete(key)
return true
})
m.controlsInitialized = false m.controlsInitialized = false
m.outputsMutex.Unlock()
m.transitionMutex.Lock() m.transitionMutex.Lock()
m.currentTemp = identityTemp m.currentTemp = identityTemp
@@ -1332,21 +1311,22 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.outputsMutex.Lock() m.outputs.Range(func(key uint32, out *outputState) bool {
for _, out := range m.outputs {
if control, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok { if control, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok {
control.Destroy() control.Destroy()
} }
} return true
m.outputs = make(map[uint32]*outputState) })
m.outputsMutex.Unlock() m.outputs.Range(func(key uint32, value *outputState) bool {
m.outputs.Delete(key)
return true
})
if manager, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1); ok { if manager, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1); ok {
manager.Destroy() manager.Destroy()
+8 -14
View File
@@ -6,8 +6,9 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
wlclient "github.com/yaslama/go-wayland/wayland/client"
) )
type Config struct { type Config struct {
@@ -48,9 +49,8 @@ type Manager struct {
registry *wlclient.Registry registry *wlclient.Registry
gammaControl interface{} gammaControl interface{}
availableOutputs []*wlclient.Output availableOutputs []*wlclient.Output
outputRegNames map[uint32]uint32 outputRegNames syncmap.Map[uint32, uint32]
outputs map[uint32]*outputState outputs syncmap.Map[uint32, *outputState]
outputsMutex sync.RWMutex
controlsInitialized bool controlsInitialized bool
cmdq chan cmd cmdq chan cmd
@@ -69,8 +69,7 @@ type Manager struct {
cachedIPLon *float64 cachedIPLon *float64
locationMutex sync.RWMutex locationMutex sync.RWMutex
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -147,19 +146,14 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock() m.subscribers.Store(id, ch)
m.subscribers[id] = ch
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock() if val, ok := m.subscribers.LoadAndDelete(id); ok {
if ch, ok := m.subscribers[id]; ok { close(val)
close(ch)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
type SharedContext struct { type SharedContext struct {
+4 -7
View File
@@ -154,14 +154,13 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
statusChan <- fmt.Errorf("configuration cancelled (outdated serial)") statusChan <- fmt.Errorf("configuration cancelled (outdated serial)")
}) })
m.headsMutex.RLock()
headsByName := make(map[string]*headState) headsByName := make(map[string]*headState)
for _, head := range m.heads { m.heads.Range(func(key uint32, head *headState) bool {
if !head.finished { if !head.finished {
headsByName[head.name] = head headsByName[head.name] = head
} }
} return true
m.headsMutex.RUnlock() })
for _, headCfg := range heads { for _, headCfg := range heads {
head, exists := headsByName[headCfg.Name] head, exists := headsByName[headCfg.Name]
@@ -188,9 +187,7 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
} }
if headCfg.ModeID != nil { if headCfg.ModeID != nil {
m.modesMutex.RLock() mode, exists := m.modes.Load(*headCfg.ModeID)
mode, exists := m.modes[*headCfg.ModeID]
m.modesMutex.RUnlock()
if !exists { if !exists {
config.Destroy() config.Destroy()
+37 -63
View File
@@ -6,20 +6,17 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
func NewManager(display *wlclient.Display) (*Manager, error) { func NewManager(display *wlclient.Display) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
ctx: display.Context(), ctx: display.Context(),
heads: make(map[uint32]*headState), cmdq: make(chan cmd, 128),
modes: make(map[uint32]*modeState), stopChan: make(chan struct{}),
cmdq: make(chan cmd, 128), dirty: make(chan struct{}, 1),
stopChan: make(chan struct{}), fatalError: make(chan error, 1),
subscribers: make(map[string]chan State),
dirty: make(chan struct{}, 1),
fatalError: make(chan error, 1),
} }
m.wg.Add(1) m.wg.Add(1)
@@ -143,9 +140,7 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
modeIDs: make([]uint32, 0), modeIDs: make([]uint32, 0),
} }
m.headsMutex.Lock() m.heads.Store(headID, head)
m.heads[headID] = head
m.headsMutex.Unlock()
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) { handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name) log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name)
@@ -254,9 +249,7 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
log.Debugf("WlrOutput: Head %d finished", headID) log.Debugf("WlrOutput: Head %d finished", headID)
head.finished = true head.finished = true
m.headsMutex.Lock() m.heads.Delete(headID)
delete(m.heads, headID)
m.headsMutex.Unlock()
m.post(func() { m.post(func() {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -279,15 +272,12 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
handle: handle, handle: handle,
} }
m.modesMutex.Lock() m.modes.Store(modeID, mode)
m.modes[modeID] = mode
m.modesMutex.Unlock()
m.headsMutex.Lock() if head, ok := m.heads.Load(headID); ok {
if head, ok := m.heads[headID]; ok {
head.modeIDs = append(head.modeIDs, modeID) head.modeIDs = append(head.modeIDs, modeID)
m.heads.Store(headID, head)
} }
m.headsMutex.Unlock()
handle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) { handle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
log.Debugf("WlrOutput: Mode %d size: %dx%d", modeID, e.Width, e.Height) log.Debugf("WlrOutput: Mode %d size: %dx%d", modeID, e.Width, e.Height)
@@ -318,9 +308,7 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
log.Debugf("WlrOutput: Mode %d finished", modeID) log.Debugf("WlrOutput: Mode %d finished", modeID)
mode.finished = true mode.finished = true
m.modesMutex.Lock() m.modes.Delete(modeID)
delete(m.modes, modeID)
m.modesMutex.Unlock()
m.post(func() { m.post(func() {
m.wlMutex.Lock() m.wlMutex.Lock()
@@ -333,22 +321,22 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
} }
func (m *Manager) updateState() { func (m *Manager) updateState() {
m.headsMutex.RLock()
m.modesMutex.RLock()
outputs := make([]Output, 0) outputs := make([]Output, 0)
for _, head := range m.heads { m.heads.Range(func(key uint32, head *headState) bool {
if head.finished { if head.finished {
continue return true
} }
modes := make([]OutputMode, 0) modes := make([]OutputMode, 0)
var currentMode *OutputMode var currentMode *OutputMode
for _, modeID := range head.modeIDs { for _, modeID := range head.modeIDs {
mode, exists := m.modes[modeID] mode, exists := m.modes.Load(modeID)
if !exists || mode.finished { if !exists {
continue
}
if mode.finished {
continue continue
} }
@@ -385,10 +373,8 @@ func (m *Manager) updateState() {
ID: head.id, ID: head.id,
} }
outputs = append(outputs, output) outputs = append(outputs, output)
} return true
})
m.modesMutex.RUnlock()
m.headsMutex.RUnlock()
newState := State{ newState := State{
Outputs: outputs, Outputs: outputs,
@@ -442,14 +428,6 @@ func (m *Manager) notifier() {
if !pending { if !pending {
continue continue
} }
m.subMutex.RLock()
subCount := len(m.subscribers)
m.subMutex.RUnlock()
if subCount == 0 {
pending = false
continue
}
currentState := m.GetState() currentState := m.GetState()
@@ -458,15 +436,14 @@ func (m *Manager) notifier() {
continue continue
} }
m.subMutex.RLock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
select { select {
case ch <- currentState: case ch <- currentState:
default: default:
log.Warn("WlrOutput: subscriber channel full, dropping update") log.Warn("WlrOutput: subscriber channel full, dropping update")
} }
} return true
m.subMutex.RUnlock() })
stateCopy := currentState stateCopy := currentState
m.lastNotified = &stateCopy m.lastNotified = &stateCopy
@@ -480,30 +457,27 @@ func (m *Manager) Close() {
m.wg.Wait() m.wg.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
m.subMutex.Lock() m.subscribers.Range(func(key string, ch chan State) bool {
for _, ch := range m.subscribers {
close(ch) close(ch)
} m.subscribers.Delete(key)
m.subscribers = make(map[string]chan State) return true
m.subMutex.Unlock() })
m.modesMutex.Lock() m.modes.Range(func(key uint32, mode *modeState) bool {
for _, mode := range m.modes {
if mode.handle != nil { if mode.handle != nil {
mode.handle.Release() mode.handle.Release()
} }
} m.modes.Delete(key)
m.modes = make(map[uint32]*modeState) return true
m.modesMutex.Unlock() })
m.headsMutex.Lock() m.heads.Range(func(key uint32, head *headState) bool {
for _, head := range m.heads {
if head.handle != nil { if head.handle != nil {
head.handle.Release() head.handle.Release()
} }
} m.heads.Delete(key)
m.heads = make(map[uint32]*headState) return true
m.headsMutex.Unlock() })
if m.manager != nil { if m.manager != nil {
m.manager.Stop() m.manager.Stop()
+13 -16
View File
@@ -4,7 +4,8 @@ import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlclient "github.com/yaslama/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type OutputMode struct { type OutputMode struct {
@@ -49,11 +50,8 @@ type Manager struct {
registry *wlclient.Registry registry *wlclient.Registry
manager *wlr_output_management.ZwlrOutputManagerV1 manager *wlr_output_management.ZwlrOutputManagerV1
headsMutex sync.RWMutex heads syncmap.Map[uint32, *headState]
heads map[uint32]*headState modes syncmap.Map[uint32, *modeState]
modesMutex sync.RWMutex
modes map[uint32]*modeState
serial uint32 serial uint32
@@ -62,8 +60,7 @@ type Manager struct {
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
subscribers map[string]chan State subscribers syncmap.Map[string, chan State]
subMutex sync.RWMutex
dirty chan struct{} dirty chan struct{}
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotified *State lastNotified *State
@@ -120,19 +117,19 @@ func (m *Manager) GetState() State {
func (m *Manager) Subscribe(id string) chan State { func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64) ch := make(chan State, 64)
m.subMutex.Lock()
m.subscribers[id] = ch m.subscribers.Store(id, ch)
m.subMutex.Unlock()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subMutex.Lock()
if ch, ok := m.subscribers[id]; ok { if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch) close(val)
delete(m.subscribers, id)
} }
m.subMutex.Unlock()
} }
func (m *Manager) notifySubscribers() { func (m *Manager) notifySubscribers() {
+3
View File
@@ -0,0 +1,3 @@
// Keep this sorted
rajveermalviya
+24
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.
+25
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
View File
@@ -0,0 +1,4 @@
#!/bin/sh
cd ./wayland
go generate -x ./...
+9
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
@@ -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
}
@@ -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
}
@@ -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)
}
}
}
}
@@ -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
}
@@ -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
+120
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)
}
@@ -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)
}
@@ -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
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
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
}
+33
View File
@@ -0,0 +1,33 @@
{
config,
lib,
pkgs,
dmsPkgs,
...
}: let
cfg = config.programs.dankMaterialShell;
in {
qmlPath = "${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms";
packages =
[
pkgs.material-symbols
pkgs.inter
pkgs.fira-code
pkgs.ddcutil
pkgs.libsForQt5.qt5ct
pkgs.kdePackages.qt6ct
dmsPkgs.dmsCli
]
++ lib.optional cfg.enableSystemMonitoring dmsPkgs.dgop
++ lib.optionals cfg.enableClipboard [pkgs.cliphist pkgs.wl-clipboard]
++ lib.optionals cfg.enableVPN [pkgs.glib pkgs.networkmanager]
++ lib.optional cfg.enableBrightnessControl pkgs.brightnessctl
++ lib.optional cfg.enableColorPicker pkgs.hyprpicker
++ lib.optional cfg.enableDynamicTheming pkgs.matugen
++ lib.optional cfg.enableAudioWavelength pkgs.cava
++ lib.optional cfg.enableCalendarEvents pkgs.khal
++ lib.optional cfg.enableSystemSound pkgs.kdePackages.qtmultimedia;
}
-170
View File
@@ -1,170 +0,0 @@
{
config,
pkgs,
lib,
dmsPkgs,
...
}: let
cfg = config.programs.dankMaterialShell;
jsonFormat = pkgs.formats.json { };
in {
imports = [
(lib.mkRemovedOptionModule ["programs" "dankMaterialShell" "enableNightMode"] "Night mode is now always available.")
(lib.mkRenamedOptionModule ["programs" "dankMaterialShell" "enableSystemd"] ["programs" "dankMaterialShell" "systemd" "enable"])
];
options.programs.dankMaterialShell = with lib.types; {
enable = lib.mkEnableOption "DankMaterialShell";
systemd = {
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
restartIfChanged = lib.mkOption {
type = bool;
default = true;
description = "Auto-restart dms.service when dankMaterialShell changes";
};
};
enableSystemMonitoring = lib.mkOption {
type = bool;
default = true;
description = "Add needed dependencies to use system monitoring widgets";
};
enableClipboard = lib.mkOption {
type = bool;
default = true;
description = "Add needed dependencies to use the clipboard widget";
};
enableVPN = lib.mkOption {
type = bool;
default = true;
description = "Add needed dependencies to use the VPN widget";
};
enableBrightnessControl = lib.mkOption {
type = bool;
default = true;
description = "Add needed dependencies to have brightness/backlight support";
};
enableColorPicker = lib.mkOption {
type = bool;
default = true;
description = "Add needed dependencies to have color picking support";
};
enableDynamicTheming = lib.mkOption {
type = bool;
default = true;
description = "Add needed dependencies to have dynamic theming support";
};
enableAudioWavelength = lib.mkOption {
type = bool;
default = true;
description = "Add needed dependencies to have audio waveleng support";
};
enableCalendarEvents = lib.mkOption {
type = bool;
default = true;
description = "Add calendar events support via khal";
};
enableSystemSound = lib.mkOption {
type = bool;
default = true;
description = "Add needed dependencies to have system sound support";
};
quickshell = {
package = lib.mkPackageOption pkgs "quickshell" {};
};
default = {
settings = lib.mkOption {
type = jsonFormat.type;
default = { };
description = "The default settings are only read if the settings.json file don't exist";
};
session = lib.mkOption {
type = jsonFormat.type;
default = { };
description = "The default session are only read if the session.json file don't exist";
};
};
plugins = lib.mkOption {
type = attrsOf (types.submodule ({ config, ... }: {
options = {
enable = lib.mkOption {
type = types.bool;
default = true;
description = "Whether to link this plugin";
};
src = lib.mkOption {
type = types.path;
description = "Source to link to DMS plugins directory";
};
};
}));
default = {};
description = "DMS Plugins to install";
};
};
config = lib.mkIf cfg.enable
{
programs.quickshell = {
enable = true;
package = cfg.quickshell.package;
configs.dms = "${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms";
};
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
Unit = {
Description = "DankMaterialShell";
PartOf = [ config.wayland.systemd.target ];
After = [ config.wayland.systemd.target ];
X-Restart-Triggers = lib.optional cfg.systemd.restartIfChanged config.programs.quickshell.configs.dms;
};
Service = {
ExecStart = lib.getExe dmsPkgs.dmsCli + " run --session";
Restart = "on-failure";
};
Install.WantedBy = [ config.wayland.systemd.target ];
};
xdg.stateFile."DankMaterialShell/default-session.json" = lib.mkIf (cfg.default.session != { }) {
source = jsonFormat.generate "default-session.json" cfg.default.session;
};
xdg.configFile = lib.mkMerge [
(lib.mapAttrs' (name: plugin: {
name = "DankMaterialShell/plugins/${name}";
value.source = plugin.src;
}) (lib.filterAttrs (n: v: v.enable) cfg.plugins))
{
"DankMaterialShell/default-settings.json" = lib.mkIf (cfg.default.settings != { }) {
source = jsonFormat.generate "default-settings.json" cfg.default.settings;
};
}
];
home.packages =
[
pkgs.material-symbols
pkgs.inter
pkgs.fira-code
pkgs.ddcutil
pkgs.libsForQt5.qt5ct
pkgs.kdePackages.qt6ct
dmsPkgs.dmsCli
]
++ lib.optional cfg.enableSystemMonitoring dmsPkgs.dgop
++ lib.optionals cfg.enableClipboard [pkgs.cliphist pkgs.wl-clipboard]
++ lib.optionals cfg.enableVPN [pkgs.glib pkgs.networkmanager]
++ lib.optional cfg.enableBrightnessControl pkgs.brightnessctl
++ lib.optional cfg.enableColorPicker pkgs.hyprpicker
++ lib.optional cfg.enableDynamicTheming pkgs.matugen
++ lib.optional cfg.enableAudioWavelength pkgs.cava
++ lib.optional cfg.enableCalendarEvents pkgs.khal
++ lib.optional cfg.enableSystemSound pkgs.kdePackages.qtmultimedia;
};
}
+11 -10
View File
@@ -11,7 +11,7 @@
user = config.services.greetd.settings.default_session.user; user = config.services.greetd.settings.default_session.user;
greeterScript = pkgs.writeShellScriptBin "dms-greeter" '' greeterScript = pkgs.writeShellScriptBin "dms-greeter" ''
export PATH=$PATH:${lib.makeBinPath [ cfg.quickshell.package config.programs.${cfg.compositor.name}.package ]} export PATH=$PATH:${lib.makeBinPath [cfg.quickshell.package config.programs.${cfg.compositor.name}.package]}
${lib.escapeShellArgs ([ ${lib.escapeShellArgs ([
"sh" "sh"
"${../../quickshell/Modules/Greetd/assets/dms-greeter}" "${../../quickshell/Modules/Greetd/assets/dms-greeter}"
@@ -28,11 +28,9 @@
])} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"} ])} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"}
''; '';
in { in {
imports = imports = let
let msg = "The option 'programs.dankMaterialShell.greeter.compositor.extraConfig' is deprecated. Please use 'programs.dankMaterialShell.greeter.compositor.customConfig' instead.";
msg = "The option 'programs.dankMaterialShell.greeter.compositor.extraConfig' is deprecated. Please use 'programs.dankMaterialShell.greeter.compositor.customConfig' instead."; in [(lib.mkRemovedOptionModule ["programs" "dankMaterialShell" "greeter" "compositor" "extraConfig"] msg)];
in
[ (lib.mkRemovedOptionModule [ "programs" "dankMaterialShell" "greeter" "compositor" "extraConfig" ] msg) ];
options.programs.dankMaterialShell.greeter = { options.programs.dankMaterialShell.greeter = {
enable = lib.mkEnableOption "DankMaterialShell greeter"; enable = lib.mkEnableOption "DankMaterialShell greeter";
@@ -77,7 +75,7 @@ in {
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
assertions = [ assertions = [
{ {
assertion = (config.users.users.${user} or { }) != { }; assertion = (config.users.users.${user} or {}) != {};
message = '' message = ''
dmsgreeter: user set for greetd default_session ${user} does not exist. Please create it before referencing it. dmsgreeter: user set for greetd default_session ${user} does not exist. Please create it before referencing it.
''; '';
@@ -95,8 +93,10 @@ in {
systemd.tmpfiles.settings."10-dmsgreeter" = { systemd.tmpfiles.settings."10-dmsgreeter" = {
"/var/lib/dmsgreeter".d = { "/var/lib/dmsgreeter".d = {
user = user; user = user;
group = if config.users.users.${user}.group != "" group =
then config.users.users.${user}.group else "greeter"; if config.users.users.${user}.group != ""
then config.users.users.${user}.group
else "greeter";
mode = "0755"; mode = "0755";
}; };
}; };
@@ -106,7 +106,8 @@ in {
if [ -f "${f}" ]; then if [ -f "${f}" ]; then
cp "${f}" . cp "${f}" .
fi fi
'') cfg.configFiles)} '')
cfg.configFiles)}
if [ -f session.json ]; then if [ -f session.json ]; then
if cp "$(${lib.getExe pkgs.jq} -r '.wallpaperPath' session.json)" wallpaper.jpg; then if cp "$(${lib.getExe pkgs.jq} -r '.wallpaperPath' session.json)" wallpaper.jpg; then
+94
View File
@@ -0,0 +1,94 @@
{
config,
pkgs,
lib,
dmsPkgs,
...
}: let
cfg = config.programs.dankMaterialShell;
jsonFormat = pkgs.formats.json {};
common = import ./common.nix {inherit config pkgs lib dmsPkgs;};
in {
imports = [
./options.nix
(lib.mkRemovedOptionModule ["programs" "dankMaterialShell" "enableNightMode"] "Night mode is now always available.")
(lib.mkRenamedOptionModule ["programs" "dankMaterialShell" "enableSystemd"] ["programs" "dankMaterialShell" "systemd" "enable"])
];
options.programs.dankMaterialShell = with lib.types; {
default = {
settings = lib.mkOption {
type = jsonFormat.type;
default = {};
description = "The default settings are only read if the settings.json file don't exist";
};
session = lib.mkOption {
type = jsonFormat.type;
default = {};
description = "The default session are only read if the session.json file don't exist";
};
};
plugins = lib.mkOption {
type = attrsOf (types.submodule ({config, ...}: {
options = {
enable = lib.mkOption {
type = types.bool;
default = true;
description = "Whether to link this plugin";
};
src = lib.mkOption {
type = types.path;
description = "Source to link to DMS plugins directory";
};
};
}));
default = {};
description = "DMS Plugins to install";
};
};
config = lib.mkIf cfg.enable
{
programs.quickshell = {
enable = true;
package = cfg.quickshell.package;
configs.dms = common.qmlPath;
};
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
Unit = {
Description = "DankMaterialShell";
PartOf = [config.wayland.systemd.target];
After = [config.wayland.systemd.target];
X-Restart-Triggers = lib.optional cfg.systemd.restartIfChanged common.qmlPath;
};
Service = {
ExecStart = lib.getExe dmsPkgs.dmsCli + " run --session";
Restart = "on-failure";
};
Install.WantedBy = [config.wayland.systemd.target];
};
xdg.stateFile."DankMaterialShell/default-session.json" = lib.mkIf (cfg.default.session != {}) {
source = jsonFormat.generate "default-session.json" cfg.default.session;
};
xdg.configFile = lib.mkMerge [
(lib.mapAttrs' (name: plugin: {
name = "DankMaterialShell/plugins/${name}";
value.source = plugin.src;
}) (lib.filterAttrs (n: v: v.enable) cfg.plugins))
{
"DankMaterialShell/default-settings.json" = lib.mkIf (cfg.default.settings != {}) {
source = jsonFormat.generate "default-settings.json" cfg.default.settings;
};
}
];
home.packages = common.packages;
};
}
+36
View File
@@ -0,0 +1,36 @@
{
config,
pkgs,
lib,
dmsPkgs,
...
}: let
cfg = config.programs.dankMaterialShell;
common = import ./common.nix {inherit config pkgs lib dmsPkgs;};
in {
imports = [
./options.nix
];
config = lib.mkIf cfg.enable
{
environment.etc."xdg/quickshell/dms".source = "${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms";
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
description = "DankMaterialShell";
path = [cfg.quickshell.package];
partOf = ["graphical-session.target"];
after = ["graphical-session.target"];
wantedBy = ["graphical-session.target"];
restartTriggers = lib.optional cfg.systemd.restartIfChanged common.qmlPath;
serviceConfig = {
ExecStart = lib.getExe dmsPkgs.dmsCli + " run --session";
Restart = "on-failure";
};
};
environment.systemPackages = [cfg.quickshell.package] ++ common.packages;
};
}
+68
View File
@@ -0,0 +1,68 @@
{
pkgs,
lib,
...
}: let
inherit (lib) types;
in {
options.programs.dankMaterialShell = {
enable = lib.mkEnableOption "DankMaterialShell";
systemd = {
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
restartIfChanged = lib.mkOption {
type = types.bool;
default = true;
description = "Auto-restart dms.service when dankMaterialShell changes";
};
};
enableSystemMonitoring = lib.mkOption {
type = types.bool;
default = true;
description = "Add needed dependencies to use system monitoring widgets";
};
enableClipboard = lib.mkOption {
type = types.bool;
default = true;
description = "Add needed dependencies to use the clipboard widget";
};
enableVPN = lib.mkOption {
type = types.bool;
default = true;
description = "Add needed dependencies to use the VPN widget";
};
enableBrightnessControl = lib.mkOption {
type = types.bool;
default = true;
description = "Add needed dependencies to have brightness/backlight support";
};
enableColorPicker = lib.mkOption {
type = types.bool;
default = true;
description = "Add needed dependencies to have color picking support";
};
enableDynamicTheming = lib.mkOption {
type = types.bool;
default = true;
description = "Add needed dependencies to have dynamic theming support";
};
enableAudioWavelength = lib.mkOption {
type = types.bool;
default = true;
description = "Add needed dependencies to have audio wavelength support";
};
enableCalendarEvents = lib.mkOption {
type = types.bool;
default = true;
description = "Add calendar events support via khal";
};
enableSystemSound = lib.mkOption {
type = types.bool;
default = true;
description = "Add needed dependencies to have system sound support";
};
quickshell = {
package = lib.mkPackageOption pkgs "quickshell" {};
};
};
}
+10 -13
View File
@@ -24,6 +24,11 @@
dgop = dgop.packages.${pkgs.stdenv.hostPlatform.system}.dgop; dgop = dgop.packages.${pkgs.stdenv.hostPlatform.system}.dgop;
dankMaterialShell = self.packages.${pkgs.stdenv.hostPlatform.system}.dankMaterialShell; dankMaterialShell = self.packages.${pkgs.stdenv.hostPlatform.system}.dankMaterialShell;
}; };
mkModuleWithDmsPkgs = path: args @ {pkgs, ...}: {
imports = [
(import path (args // {dmsPkgs = buildDmsPkgs pkgs;}))
];
};
in { in {
formatter = forEachSystem (_: pkgs: pkgs.alejandra); formatter = forEachSystem (_: pkgs: pkgs.alejandra);
@@ -47,7 +52,7 @@
pname = "dmsCli"; pname = "dmsCli";
src = ./core; src = ./core;
vendorHash = "sha256-ZbBRV3HOMxbq25Pt/hArKbuyES3j3bbb2kOiLEkCahA="; vendorHash = "sha256-nc4CvEPfJ6l16/zmhnXr1jqpi6BeSXd3g/51djbEfpQ=";
subPackages = ["cmd/dms"]; subPackages = ["cmd/dms"];
@@ -81,20 +86,12 @@
} }
); );
homeModules.dankMaterialShell.default = {pkgs, ...}: let homeModules.dankMaterialShell.default = mkModuleWithDmsPkgs ./distro/nix/home.nix;
dmsPkgs = buildDmsPkgs pkgs;
in {
imports = [./distro/nix/default.nix];
_module.args.dmsPkgs = dmsPkgs;
};
homeModules.dankMaterialShell.niri = import ./distro/nix/niri.nix; homeModules.dankMaterialShell.niri = import ./distro/nix/niri.nix;
nixosModules.greeter = {pkgs, ...}: let nixosModules.dankMaterialShell = mkModuleWithDmsPkgs ./distro/nix/nixos.nix;
dmsPkgs = buildDmsPkgs pkgs;
in { nixosModules.greeter = mkModuleWithDmsPkgs ./distro/nix/greeter.nix;
imports = [./distro/nix/greeter.nix];
_module.args.dmsPkgs = dmsPkgs;
};
}; };
} }
+4
View File
@@ -12,5 +12,9 @@ Singleton {
if (!modal.allowStacking) { if (!modal.allowStacking) {
closeAllModalsExcept(modal) closeAllModalsExcept(modal)
} }
if (!modal.keepPopoutsOpen) {
PopoutManager.closeAllPopouts()
}
TrayMenuManager.closeAllMenus()
} }
} }
+164
View File
@@ -0,0 +1,164 @@
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
}
if (currentPopout !== popout) {
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()
}
}
}
}
+199 -31
View File
@@ -422,29 +422,59 @@ Singleton {
} }
function setMonitorWallpaper(screenName, path) { function setMonitorWallpaper(screenName, path) {
var newMonitorWallpapers = Object.assign({}, monitorWallpapers) var screen = null
if (path && path !== "") { var screens = Quickshell.screens
newMonitorWallpapers[screenName] = path for (var i = 0; i < screens.length; i++) {
} else { if (screens[i].name === screenName) {
delete newMonitorWallpapers[screenName] screen = screens[i]
break
}
} }
if (!screen) {
console.warn("SessionData: Screen not found:", screenName)
return
}
var identifier = typeof SettingsData !== "undefined" ? SettingsData.getScreenDisplayName(screen) : screen.name
var newMonitorWallpapers = {}
for (var key in monitorWallpapers) {
var isThisScreen = key === screen.name || (screen.model && key === screen.model)
if (!isThisScreen) {
newMonitorWallpapers[key] = monitorWallpapers[key]
}
}
if (path && path !== "") {
newMonitorWallpapers[identifier] = path
}
monitorWallpapers = newMonitorWallpapers monitorWallpapers = newMonitorWallpapers
if (perModeWallpaper) { if (perModeWallpaper) {
if (isLightMode) { if (isLightMode) {
var newLight = Object.assign({}, monitorWallpapersLight) var newLight = {}
for (var key in monitorWallpapersLight) {
var isThisScreen = key === screen.name || (screen.model && key === screen.model)
if (!isThisScreen) {
newLight[key] = monitorWallpapersLight[key]
}
}
if (path && path !== "") { if (path && path !== "") {
newLight[screenName] = path newLight[identifier] = path
} else {
delete newLight[screenName]
} }
monitorWallpapersLight = newLight monitorWallpapersLight = newLight
} else { } else {
var newDark = Object.assign({}, monitorWallpapersDark) var newDark = {}
for (var key in monitorWallpapersDark) {
var isThisScreen = key === screen.name || (screen.model && key === screen.model)
if (!isThisScreen) {
newDark[key] = monitorWallpapersDark[key]
}
}
if (path && path !== "") { if (path && path !== "") {
newDark[screenName] = path newDark[identifier] = path
} else {
delete newDark[screenName]
} }
monitorWallpapersDark = newDark monitorWallpapersDark = newDark
} }
@@ -489,61 +519,153 @@ Singleton {
} }
function setMonitorCyclingEnabled(screenName, enabled) { function setMonitorCyclingEnabled(screenName, enabled) {
var newSettings = Object.assign({}, monitorCyclingSettings) var screen = null
if (!newSettings[screenName]) { var screens = Quickshell.screens
newSettings[screenName] = { for (var i = 0; i < screens.length; i++) {
if (screens[i].name === screenName) {
screen = screens[i]
break
}
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName)
return
}
var identifier = typeof SettingsData !== "undefined" ? SettingsData.getScreenDisplayName(screen) : screen.name
var newSettings = {}
for (var key in monitorCyclingSettings) {
var isThisScreen = key === screen.name || (screen.model && key === screen.model)
if (!isThisScreen) {
newSettings[key] = monitorCyclingSettings[key]
}
}
if (!newSettings[identifier]) {
newSettings[identifier] = {
"enabled": false, "enabled": false,
"mode": "interval", "mode": "interval",
"interval": 300, "interval": 300,
"time": "06:00" "time": "06:00"
} }
} }
newSettings[screenName].enabled = enabled newSettings[identifier].enabled = enabled
monitorCyclingSettings = newSettings monitorCyclingSettings = newSettings
saveSettings() saveSettings()
} }
function setMonitorCyclingMode(screenName, mode) { function setMonitorCyclingMode(screenName, mode) {
var newSettings = Object.assign({}, monitorCyclingSettings) var screen = null
if (!newSettings[screenName]) { var screens = Quickshell.screens
newSettings[screenName] = { for (var i = 0; i < screens.length; i++) {
if (screens[i].name === screenName) {
screen = screens[i]
break
}
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName)
return
}
var identifier = typeof SettingsData !== "undefined" ? SettingsData.getScreenDisplayName(screen) : screen.name
var newSettings = {}
for (var key in monitorCyclingSettings) {
var isThisScreen = key === screen.name || (screen.model && key === screen.model)
if (!isThisScreen) {
newSettings[key] = monitorCyclingSettings[key]
}
}
if (!newSettings[identifier]) {
newSettings[identifier] = {
"enabled": false, "enabled": false,
"mode": "interval", "mode": "interval",
"interval": 300, "interval": 300,
"time": "06:00" "time": "06:00"
} }
} }
newSettings[screenName].mode = mode newSettings[identifier].mode = mode
monitorCyclingSettings = newSettings monitorCyclingSettings = newSettings
saveSettings() saveSettings()
} }
function setMonitorCyclingInterval(screenName, interval) { function setMonitorCyclingInterval(screenName, interval) {
var newSettings = Object.assign({}, monitorCyclingSettings) var screen = null
if (!newSettings[screenName]) { var screens = Quickshell.screens
newSettings[screenName] = { for (var i = 0; i < screens.length; i++) {
if (screens[i].name === screenName) {
screen = screens[i]
break
}
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName)
return
}
var identifier = typeof SettingsData !== "undefined" ? SettingsData.getScreenDisplayName(screen) : screen.name
var newSettings = {}
for (var key in monitorCyclingSettings) {
var isThisScreen = key === screen.name || (screen.model && key === screen.model)
if (!isThisScreen) {
newSettings[key] = monitorCyclingSettings[key]
}
}
if (!newSettings[identifier]) {
newSettings[identifier] = {
"enabled": false, "enabled": false,
"mode": "interval", "mode": "interval",
"interval": 300, "interval": 300,
"time": "06:00" "time": "06:00"
} }
} }
newSettings[screenName].interval = interval newSettings[identifier].interval = interval
monitorCyclingSettings = newSettings monitorCyclingSettings = newSettings
saveSettings() saveSettings()
} }
function setMonitorCyclingTime(screenName, time) { function setMonitorCyclingTime(screenName, time) {
var newSettings = Object.assign({}, monitorCyclingSettings) var screen = null
if (!newSettings[screenName]) { var screens = Quickshell.screens
newSettings[screenName] = { for (var i = 0; i < screens.length; i++) {
if (screens[i].name === screenName) {
screen = screens[i]
break
}
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName)
return
}
var identifier = typeof SettingsData !== "undefined" ? SettingsData.getScreenDisplayName(screen) : screen.name
var newSettings = {}
for (var key in monitorCyclingSettings) {
var isThisScreen = key === screen.name || (screen.model && key === screen.model)
if (!isThisScreen) {
newSettings[key] = monitorCyclingSettings[key]
}
}
if (!newSettings[identifier]) {
newSettings[identifier] = {
"enabled": false, "enabled": false,
"mode": "interval", "mode": "interval",
"interval": 300, "interval": 300,
"time": "06:00" "time": "06:00"
} }
} }
newSettings[screenName].time = time newSettings[identifier].time = time
monitorCyclingSettings = newSettings monitorCyclingSettings = newSettings
saveSettings() saveSettings()
} }
@@ -770,11 +892,57 @@ Singleton {
if (!perMonitorWallpaper) { if (!perMonitorWallpaper) {
return wallpaperPath return wallpaperPath
} }
return monitorWallpapers[screenName] || wallpaperPath
var screen = null
var screens = Quickshell.screens
for (var i = 0; i < screens.length; i++) {
if (screens[i].name === screenName) {
screen = screens[i]
break
}
}
if (!screen) {
return monitorWallpapers[screenName] || wallpaperPath
}
if (monitorWallpapers[screen.name]) {
return monitorWallpapers[screen.name]
}
if (screen.model && monitorWallpapers[screen.model]) {
return monitorWallpapers[screen.model]
}
return wallpaperPath
} }
function getMonitorCyclingSettings(screenName) { function getMonitorCyclingSettings(screenName) {
return monitorCyclingSettings[screenName] || { var screen = null
var screens = Quickshell.screens
for (var i = 0; i < screens.length; i++) {
if (screens[i].name === screenName) {
screen = screens[i]
break
}
}
if (!screen) {
return monitorCyclingSettings[screenName] || {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
}
}
if (monitorCyclingSettings[screen.name]) {
return monitorCyclingSettings[screen.name]
}
if (screen.model && monitorCyclingSettings[screen.model]) {
return monitorCyclingSettings[screen.model]
}
return {
"enabled": false, "enabled": false,
"mode": "interval", "mode": "interval",
"interval": 300, "interval": 300,
+85 -3
View File
@@ -23,7 +23,11 @@ Singleton {
Top, Top,
Bottom, Bottom,
Left, Left,
Right Right,
TopCenter,
BottomCenter,
LeftCenter,
RightCenter
} }
enum AnimationSpeed { enum AnimationSpeed {
@@ -305,6 +309,13 @@ Singleton {
property int notificationPopupPosition: SettingsData.Position.Top property int notificationPopupPosition: SettingsData.Position.Top
property bool osdAlwaysShowValue: false property bool osdAlwaysShowValue: false
property int osdPosition: SettingsData.Position.BottomCenter
property bool osdVolumeEnabled: true
property bool osdBrightnessEnabled: true
property bool osdIdleInhibitorEnabled: true
property bool osdMicMuteEnabled: true
property bool osdCapsLockEnabled: true
property bool osdPowerProfileEnabled: true
property bool powerActionConfirm: true property bool powerActionConfirm: true
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
@@ -321,6 +332,7 @@ Singleton {
property string updaterCustomCommand: "" property string updaterCustomCommand: ""
property string updaterTerminalAdditionalParams: "" property string updaterTerminalAdditionalParams: ""
property string displayNameMode: "system"
property var screenPreferences: ({}) property var screenPreferences: ({})
property var showOnLastDisplay: ({}) property var showOnLastDisplay: ({})
@@ -584,12 +596,82 @@ 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 getScreenDisplayName(screen) {
if (!screen) return ""
if (displayNameMode === "model" && screen.model) {
return screen.model
}
return screen.name
}
function isScreenInPreferences(screen, prefs) {
if (!screen) return false
return prefs.some(pref => {
if (typeof pref === "string") {
return pref === "all" || pref === screen.name || pref === screen.model
}
if (displayNameMode === "model") {
return pref.model && screen.model && pref.model === screen.model
}
return pref.name === screen.name
})
}
function getFilteredScreens(componentId) { function getFilteredScreens(componentId) {
var prefs = screenPreferences && screenPreferences[componentId] || ["all"] var prefs = screenPreferences && screenPreferences[componentId] || ["all"]
if (prefs.includes("all")) { if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) {
return Quickshell.screens return Quickshell.screens
} }
var filtered = Quickshell.screens.filter(screen => prefs.includes(screen.name)) var filtered = Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs))
if (filtered.length === 0 && showOnLastDisplay && showOnLastDisplay[componentId] && Quickshell.screens.length === 1) { if (filtered.length === 0 && showOnLastDisplay && showOnLastDisplay[componentId] && Quickshell.screens.length === 1) {
return Quickshell.screens return Quickshell.screens
} }
+36 -7
View File
@@ -421,15 +421,44 @@ Singleton {
} }
return typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12 return typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12
} }
property string fontFamily: {
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.fontFamily
}
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable"
}
property string monoFontFamily: {
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.monoFontFamily
}
return typeof SettingsData !== "undefined" ? SettingsData.monoFontFamily : "Fira Code"
}
property int fontWeight: {
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.fontWeight
}
return typeof SettingsData !== "undefined" ? SettingsData.fontWeight : Font.Normal
}
property real fontScale: {
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.fontScale
}
return typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0
}
property real spacingXS: 4 property real spacingXS: 4
property real spacingS: 8 property real spacingS: 8
property real spacingM: 12 property real spacingM: 12
property real spacingL: 16 property real spacingL: 16
property real spacingXL: 24 property real spacingXL: 24
property real fontSizeSmall: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 12 property real fontSizeSmall: Math.round(fontScale * 12)
property real fontSizeMedium: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 14 property real fontSizeMedium: Math.round(fontScale * 14)
property real fontSizeLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 16 property real fontSizeLarge: Math.round(fontScale * 16)
property real fontSizeXLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 20 property real fontSizeXLarge: Math.round(fontScale * 20)
property real barHeight: 48 property real barHeight: 48
property real iconSize: 24 property real iconSize: 24
property real iconSizeSmall: 16 property real iconSizeSmall: 16
@@ -650,10 +679,10 @@ Singleton {
const scale = barThickness / 48 const scale = barThickness / 48
const dankBarScale = (typeof SettingsData !== "undefined" ? SettingsData.dankBarFontScale : 1.0) const dankBarScale = (typeof SettingsData !== "undefined" ? SettingsData.dankBarFontScale : 1.0)
if (scale <= 0.75) if (scale <= 0.75)
return fontSizeSmall * 0.9 * dankBarScale return Math.round(fontSizeSmall * 0.9 * dankBarScale)
if (scale >= 1.25) if (scale >= 1.25)
return fontSizeMedium * dankBarScale return Math.round(fontSizeMedium * dankBarScale)
return fontSizeSmall * dankBarScale return Math.round(fontSizeSmall * dankBarScale)
} }
function getBatteryIcon(level, isCharging, batteryAvailable) { function getBatteryIcon(level, isCharging, batteryAvailable) {
+32
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
}
}
}
}
@@ -215,6 +215,13 @@ var SPEC = {
notificationPopupPosition: { def: 0 }, notificationPopupPosition: { def: 0 },
osdAlwaysShowValue: { def: false }, osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 },
osdVolumeEnabled: { def: true },
osdBrightnessEnabled: { def: true },
osdIdleInhibitorEnabled: { def: true },
osdMicMuteEnabled: { def: true },
osdCapsLockEnabled: { def: true },
osdPowerProfileEnabled: { def: true },
powerActionConfirm: { def: true }, powerActionConfirm: { def: true },
powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] }, powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] },
@@ -231,6 +238,7 @@ var SPEC = {
updaterCustomCommand: { def: "" }, updaterCustomCommand: { def: "" },
updaterTerminalAdditionalParams: { def: "" }, updaterTerminalAdditionalParams: { def: "" },
displayNameMode: { def: "system" },
screenPreferences: { def: {} }, screenPreferences: { def: {} },
showOnLastDisplay: { def: {} } showOnLastDisplay: { def: {} }
}; };
+8 -42
View File
@@ -217,6 +217,14 @@ Item {
id: polkitAuthModal id: polkitAuthModal
} }
BluetoothPairingModal {
id: bluetoothPairingModal
Component.onCompleted: {
PopoutService.bluetoothPairingModal = bluetoothPairingModal
}
}
property string lastCredentialsToken: "" property string lastCredentialsToken: ""
property var lastCredentialsTime: 0 property var lastCredentialsTime: 0
@@ -297,48 +305,6 @@ Item {
} }
} }
LazyLoader {
id: powerMenuLoader
active: false
PowerMenu {
id: powerMenu
onPowerActionRequested: (action, title, message) => {
if (SettingsData.powerActionConfirm) {
powerConfirmModalLoader.active = true
if (powerConfirmModalLoader.item) {
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
powerConfirmModalLoader.item.show(title, message, () => actionApply(action), function () {})
}
} else {
actionApply(action)
}
}
function actionApply(action) {
switch (action) {
case "logout":
SessionService.logout()
break
case "suspend":
SessionService.suspend()
break
case "hibernate":
SessionService.hibernate()
break
case "reboot":
SessionService.reboot()
break
case "poweroff":
SessionService.poweroff()
break
}
}
}
}
LazyLoader { LazyLoader {
id: powerConfirmModalLoader id: powerConfirmModalLoader
+64 -18
View File
@@ -1,4 +1,6 @@
import QtQuick import QtQuick
import Quickshell
import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -9,6 +11,11 @@ DankModal {
layerNamespace: "dms:bluetooth-pairing" layerNamespace: "dms:bluetooth-pairing"
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string deviceName: "" property string deviceName: ""
property string deviceAddress: "" property string deviceAddress: ""
property string requestType: "" property string requestType: ""
@@ -18,6 +25,7 @@ DankModal {
property string passkeyInput: "" property string passkeyInput: ""
function show(pairingData) { function show(pairingData) {
console.log("BluetoothPairingModal.show() called:", JSON.stringify(pairingData))
token = pairingData.token || "" token = pairingData.token || ""
deviceName = pairingData.deviceName || "" deviceName = pairingData.deviceName || ""
deviceAddress = pairingData.deviceAddr || "" deviceAddress = pairingData.deviceAddr || ""
@@ -26,6 +34,7 @@ DankModal {
pinInput = "" pinInput = ""
passkeyInput = "" passkeyInput = ""
console.log("BluetoothPairingModal: Calling open()")
open() open()
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item) { if (contentLoader.item) {
@@ -39,6 +48,8 @@ DankModal {
} }
shouldBeVisible: false shouldBeVisible: false
allowStacking: true
keepPopoutsOpen: true
width: 420 width: 420
height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240 height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240
@@ -62,8 +73,11 @@ DankModal {
} }
onBackgroundClicked: () => { onBackgroundClicked: () => {
DMSService.bluetoothCancelPairing(token) if (token) {
DMSService.bluetoothCancelPairing(token)
}
close() close()
token = ""
pinInput = "" pinInput = ""
passkeyInput = "" passkeyInput = ""
} }
@@ -80,8 +94,11 @@ DankModal {
implicitHeight: mainColumn.implicitHeight implicitHeight: mainColumn.implicitHeight
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
DMSService.bluetoothCancelPairing(token) if (token) {
DMSService.bluetoothCancelPairing(token)
}
close() close()
token = ""
pinInput = "" pinInput = ""
passkeyInput = "" passkeyInput = ""
event.accepted = true event.accepted = true
@@ -110,17 +127,22 @@ DankModal {
StyledText { StyledText {
text: { text: {
if (requestType === "confirm") switch (requestType) {
case "confirm":
return I18n.tr("Confirm passkey for ") + deviceName return I18n.tr("Confirm passkey for ") + deviceName
if (requestType === "authorize") case "display-passkey":
return I18n.tr("Enter this passkey on ") + deviceName
case "authorize":
return I18n.tr("Authorize pairing with ") + deviceName return I18n.tr("Authorize pairing with ") + deviceName
if (requestType.startsWith("authorize-service")) case "pin":
return I18n.tr("Authorize service for ") + deviceName
if (requestType === "pin")
return I18n.tr("Enter PIN for ") + deviceName return I18n.tr("Enter PIN for ") + deviceName
if (requestType === "passkey") case "passkey":
return I18n.tr("Enter passkey for ") + deviceName return I18n.tr("Enter passkey for ") + deviceName
return deviceName default:
if (requestType.startsWith("authorize-service"))
return I18n.tr("Authorize service for ") + deviceName
return deviceName
}
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
@@ -204,7 +226,7 @@ DankModal {
height: 56 height: 56
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
visible: requestType === "confirm" visible: requestType === "confirm" || requestType === "display-passkey"
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
@@ -261,8 +283,11 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: () => { onClicked: () => {
DMSService.bluetoothCancelPairing(token) if (token) {
DMSService.bluetoothCancelPairing(token)
}
close() close()
token = ""
pinInput = "" pinInput = ""
passkeyInput = "" passkeyInput = ""
} }
@@ -288,11 +313,17 @@ DankModal {
anchors.centerIn: parent anchors.centerIn: parent
text: { text: {
if (requestType === "confirm") switch (requestType) {
case "confirm":
case "display-passkey":
return I18n.tr("Confirm") return I18n.tr("Confirm")
if (requestType === "authorize" || requestType.startsWith("authorize-service")) case "authorize":
return I18n.tr("Authorize") return I18n.tr("Authorize")
return I18n.tr("Pair") default:
if (requestType.startsWith("authorize-service"))
return I18n.tr("Authorize")
return I18n.tr("Pair")
}
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.background color: Theme.background
@@ -331,8 +362,11 @@ DankModal {
iconSize: Theme.iconSize - 4 iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
onClicked: () => { onClicked: () => {
DMSService.bluetoothCancelPairing(token) if (token) {
DMSService.bluetoothCancelPairing(token)
}
close() close()
token = ""
pinInput = "" pinInput = ""
passkeyInput = "" passkeyInput = ""
} }
@@ -343,12 +377,23 @@ DankModal {
function submitPairing() { function submitPairing() {
const secrets = {} const secrets = {}
if (requestType === "pin") { switch (requestType) {
case "pin":
secrets["pin"] = pinInput secrets["pin"] = pinInput
} else if (requestType === "passkey") { break
case "passkey":
secrets["passkey"] = passkeyInput secrets["passkey"] = passkeyInput
} else if (requestType === "confirm" || requestType === "authorize" || requestType.startsWith("authorize-service")) { break
case "confirm":
case "display-passkey":
case "authorize":
secrets["decision"] = "yes" secrets["decision"] = "yes"
break
default:
if (requestType.startsWith("authorize-service")) {
secrets["decision"] = "yes"
}
break
} }
DMSService.bluetoothSubmitPairing(token, secrets, true, response => { DMSService.bluetoothSubmitPairing(token, secrets, true, response => {
@@ -358,6 +403,7 @@ DankModal {
}) })
close() close()
token = ""
pinInput = "" pinInput = ""
passkeyInput = "" passkeyInput = ""
} }
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
@@ -14,6 +15,11 @@ DankModal {
layerNamespace: "dms:clipboard" layerNamespace: "dms:clipboard"
HyprlandFocusGrab {
windows: [clipboardHistoryModal]
active: CompositorService.isHyprland && clipboardHistoryModal.shouldHaveFocus
}
property int totalCount: 0 property int totalCount: 0
property var clipboardEntries: [] property var clipboardEntries: []
property string searchText: "" property string searchText: ""
@@ -6,6 +6,8 @@ import qs.Widgets
DankModal { DankModal {
id: root id: root
layerNamespace: "dms:confirm-modal"
property string confirmTitle: "" property string confirmTitle: ""
property string confirmMessage: "" property string confirmMessage: ""
property string confirmButtonText: "Confirm" property string confirmButtonText: "Confirm"
+8 -2
View File
@@ -43,6 +43,7 @@ PanelWindow {
property bool allowFocusOverride: false property bool allowFocusOverride: false
property bool allowStacking: false property bool allowStacking: false
property bool keepContentLoaded: false property bool keepContentLoaded: false
property bool keepPopoutsOpen: false
signal opened signal opened
signal dialogClosed signal dialogClosed
@@ -88,7 +89,12 @@ PanelWindow {
} }
} }
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: {
if (!shouldHaveFocus) return WlrKeyboardFocus.None
if (CompositorService.isHyprland) return WlrKeyboardFocus.OnDemand
return WlrKeyboardFocus.Exclusive
}
onVisibleChanged: { onVisibleChanged: {
if (root.visible) { if (root.visible) {
opened() opened()
@@ -234,7 +240,7 @@ PanelWindow {
clip: false clip: false
layer.enabled: true layer.enabled: true
layer.smooth: false layer.smooth: false
layer.textureSize: Qt.size(width * root.dpr, height * root.dpr) layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
opacity: root.shouldBeVisible ? 1 : 0 opacity: root.shouldBeVisible ? 1 : 0
scale: modalContainer.scaleValue scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5, root.dpr) x: Theme.snap(modalContainer.animX + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
+9 -3
View File
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
@@ -12,6 +13,11 @@ DankModal {
layerNamespace: "dms:color-picker" layerNamespace: "dms:color-picker"
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string pickerTitle: "Choose Color" property string pickerTitle: "Choose Color"
property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary
property var onColorSelectedCallback: null property var onColorSelectedCallback: null
@@ -60,7 +66,7 @@ DankModal {
} }
function copyColorToClipboard(colorValue) { function copyColorToClipboard(colorValue) {
Quickshell.execDetached(["sh", "-c", `echo "${colorValue}" | wl-copy`]) Quickshell.execDetached(["sh", "-c", `echo -n "${colorValue}" | wl-copy`])
ToastService.showInfo(`Color ${colorValue} copied`) ToastService.showInfo(`Color ${colorValue} copied`)
SessionData.addRecentColor(currentColor) SessionData.addRecentColor(currentColor)
} }
@@ -571,7 +577,7 @@ DankModal {
} else { } else {
rgbString = `rgb(${r}, ${g}, ${b})` rgbString = `rgb(${r}, ${g}, ${b})`
} }
Quickshell.execDetached(["sh", "-c", `echo "${rgbString}" | wl-copy`]) Quickshell.execDetached(["sh", "-c", `echo -n "${rgbString}" | wl-copy`])
ToastService.showInfo(`${rgbString} copied`) ToastService.showInfo(`${rgbString} copied`)
} }
} }
@@ -635,7 +641,7 @@ DankModal {
} else { } else {
hsvString = `${h}, ${s}, ${v}` hsvString = `${h}, ${s}, ${v}`
} }
Quickshell.execDetached(["sh", "-c", `echo "${hsvString}" | wl-copy`]) Quickshell.execDetached(["sh", "-c", `echo -n "${hsvString}" | wl-copy`])
ToastService.showInfo(`HSV ${hsvString} copied`) ToastService.showInfo(`HSV ${hsvString} copied`)
} }
} }
@@ -2,10 +2,13 @@ import Qt.labs.folderlistmodel
import QtCore import QtCore
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets import qs.Widgets
DankModal { DankModal {
@@ -13,6 +16,13 @@ DankModal {
layerNamespace: "dms:file-browser" layerNamespace: "dms:file-browser"
HyprlandFocusGrab {
windows: [fileBrowserModal]
active: CompositorService.isHyprland && fileBrowserModal.shouldHaveFocus
}
keepPopoutsOpen: true
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation) property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation) property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation) property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation)
+7
View File
@@ -1,4 +1,6 @@
import QtQuick import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
@@ -11,6 +13,11 @@ DankModal {
layerNamespace: "dms:notification-center-modal" layerNamespace: "dms:notification-center-modal"
HyprlandFocusGrab {
windows: [notificationModal]
active: CompositorService.isHyprland && notificationModal.shouldHaveFocus
}
property bool notificationModalOpen: false property bool notificationModalOpen: false
property var notificationListRef: null property var notificationListRef: null
+7
View File
@@ -1,4 +1,6 @@
import QtQuick import QtQuick
import Quickshell
import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -9,6 +11,11 @@ DankModal {
layerNamespace: "dms:polkit" layerNamespace: "dms:polkit"
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string passwordInput: "" property string passwordInput: ""
property var currentFlow: PolkitService.agent?.flow property var currentFlow: PolkitService.agent?.flow
property bool isLoading: false property bool isLoading: false
+8
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -10,6 +11,11 @@ DankModal {
layerNamespace: "dms:power-menu" layerNamespace: "dms:power-menu"
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property int selectedIndex: 0 property int selectedIndex: 0
property int selectedRow: 0 property int selectedRow: 0
property int selectedCol: 0 property int selectedCol: 0
@@ -33,7 +39,9 @@ DankModal {
parentBounds = bounds parentBounds = bounds
parentScreen = targetScreen parentScreen = targetScreen
backgroundOpacity = 0 backgroundOpacity = 0
keepPopoutsOpen = true
open() open()
keepPopoutsOpen = false
} }
function updateVisibleActions() { function updateVisibleActions() {
@@ -139,7 +139,7 @@ Rectangle {
} }
StyledText { StyledText {
text: DgopService.distribution || "Linux" text: DgopService.hostname || "DMS"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
@@ -12,9 +12,9 @@ FocusScope {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: 0 anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS anchors.rightMargin: 0
anchors.bottomMargin: Theme.spacingM anchors.bottomMargin: 0
anchors.topMargin: 0 anchors.topMargin: 0
color: "transparent" color: "transparent"
@@ -1,5 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
@@ -13,6 +15,11 @@ DankModal {
layerNamespace: "dms:settings" layerNamespace: "dms:settings"
HyprlandFocusGrab {
windows: [settingsModal]
active: CompositorService.isHyprland && settingsModal.shouldHaveFocus
}
property Component settingsContent property Component settingsContent
property alias profileBrowser: profileBrowser property alias profileBrowser: profileBrowser
property int currentTabIndex: 0 property int currentTabIndex: 0
@@ -66,7 +66,7 @@ Rectangle {
Column { Column {
id: sidebarColumn id: sidebarColumn
width: parent.width anchors.fill: parent
anchors.leftMargin: Theme.spacingS anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS anchors.bottomMargin: Theme.spacingS
@@ -100,7 +100,7 @@ Rectangle {
property bool isActive: sidebarContainer.currentIndex === index property bool isActive: sidebarContainer.currentIndex === index
width: sidebarColumn.width - Theme.spacingS * 2 width: parent.width
height: 44 height: 44
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent" color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import Quickshell.Widgets import Quickshell.Widgets
import qs.Common import qs.Common
@@ -14,6 +15,11 @@ DankModal {
layerNamespace: "dms:spotlight" layerNamespace: "dms:spotlight"
HyprlandFocusGrab {
windows: [spotlightModal]
active: CompositorService.isHyprland && spotlightModal.shouldHaveFocus
}
property bool spotlightOpen: false property bool spotlightOpen: false
property alias spotlightContent: spotlightContentInstance property alias spotlightContent: spotlightContentInstance

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