mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-13 07:42:46 -04:00
Compare commits
71 Commits
v0.5.2
...
4eb896629d
| Author | SHA1 | Date | |
|---|---|---|---|
| 4eb896629d | |||
| b310e66275 | |||
| b39da1bea7 | |||
| fa575d0574 | |||
| dfe2f3771b | |||
| 46caeb0445 | |||
| 59cc9c7006 | |||
| 12e91534eb | |||
| d9da88ceb5 | |||
| 2dbfec0307 | |||
| 09cf8c9641 | |||
| f1bed4d6a3 | |||
| 2ed6c33c83 | |||
| 7ad532ed17 | |||
| 92fe8c5b14 | |||
| 8e95572589 | |||
| 62da862a66 | |||
| 993e34f548 | |||
| e39465aece | |||
| 8fd616b680 | |||
| cc054b27de | |||
| dfdaa82245 | |||
| 99a307e0ad | |||
| 5ddea836a1 | |||
| 208d92aa06 | |||
| 6ef9ddd4f3 | |||
| 1c92d39185 | |||
| c0f072217c | |||
| 542562f988 | |||
| 4e6f0d5e87 | |||
| 10639a5ead | |||
| 06d668e710 | |||
| d1472dfcba | |||
| ccb4da3cd8 | |||
| 46e96b49f0 | |||
| 984cfe7f98 | |||
| d769300137 | |||
| d175d66828 | |||
| c1a314332e | |||
| 046ac59d21 | |||
| 00c06f07d0 | |||
| 3e2ab40c6a | |||
| 350ffd0052 | |||
| ecd1a622d2 | |||
| f13968aa61 | |||
| 4d1ffde54c | |||
| d69017a706 | |||
| f2deaeccdb | |||
| ea9b0d2a79 | |||
| 2e6dbedb8b | |||
| 6f359df8f9 | |||
| f6db20cd06 | |||
| 6287fae065 | |||
| e441607ce3 | |||
| b5379a95fa | |||
| 64ec5be919 | |||
| 3916512d66 | |||
| e2f426a1bd | |||
| aa1df8dfcf | |||
| 67557555f2 | |||
| 4cb652abd9 | |||
| d11868b99f | |||
| 1798417e6a | |||
| 43dc3e5bb1 | |||
| 91891a14ed | |||
| 20f7d60147 | |||
| 7e17e7d37a | |||
| cbb244f785 | |||
| 1c264d858b | |||
| 217037c2ae | |||
| b4dbd0b69c |
@@ -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
|
||||||
|
|
||||||
|
[](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.
|
||||||
|
|||||||
@@ -368,6 +368,7 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
pluginsCmd,
|
pluginsCmd,
|
||||||
dank16Cmd,
|
dank16Cmd,
|
||||||
brightnessCmd,
|
brightnessCmd,
|
||||||
|
dpmsCmd,
|
||||||
keybindsCmd,
|
keybindsCmd,
|
||||||
greeterCmd,
|
greeterCmd,
|
||||||
setupCmd,
|
setupCmd,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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, ¤tState) {
|
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, ¤tState) {
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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, ¤tState) {
|
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, ¤tState) {
|
||||||
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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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, ¤tState) {
|
if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, ¤tState) {
|
||||||
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()
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
@@ -125,8 +126,9 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
connType, displayName, vpnSvc := readConnTypeAndName(conn)
|
connType, displayName, vpnSvc := readConnTypeAndName(conn)
|
||||||
ssid := readSSID(conn)
|
ssid := readSSID(conn)
|
||||||
fields := fieldsNeeded(settingName, hints)
|
fields := fieldsNeeded(settingName, hints)
|
||||||
|
vpnPasswordFlags := readVPNPasswordFlags(conn, settingName)
|
||||||
|
|
||||||
log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d", connType, displayName, vpnSvc, fields, flags)
|
log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d, vpnPasswordFlags=%d", connType, displayName, vpnSvc, fields, flags, vpnPasswordFlags)
|
||||||
|
|
||||||
if a.backend != nil {
|
if a.backend != nil {
|
||||||
a.backend.stateMutex.RLock()
|
a.backend.stateMutex.RLock()
|
||||||
@@ -163,57 +165,70 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(fields) == 0 {
|
if len(fields) == 0 {
|
||||||
// For VPN connections with no hints, we can't provide a proper UI.
|
|
||||||
// Defer to other agents (like nm-applet or VPN-specific auth dialogs)
|
|
||||||
// that can handle the VPN type properly (e.g., OpenConnect with SAML, etc.)
|
|
||||||
if settingName == "vpn" {
|
if settingName == "vpn" {
|
||||||
log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc)
|
if a.backend != nil {
|
||||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
a.backend.stateMutex.RLock()
|
||||||
}
|
isConnectingVPN := a.backend.state.IsConnectingVPN
|
||||||
|
a.backend.stateMutex.RUnlock()
|
||||||
|
|
||||||
const (
|
if !isConnectingVPN {
|
||||||
NM_SETTING_SECRET_FLAG_NONE = 0
|
log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc)
|
||||||
NM_SETTING_SECRET_FLAG_AGENT_OWNED = 1
|
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
||||||
NM_SETTING_SECRET_FLAG_NOT_SAVED = 2
|
}
|
||||||
NM_SETTING_SECRET_FLAG_NOT_REQUIRED = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
var passwordFlags uint32 = 0xFFFF
|
log.Infof("[SecretAgent] VPN with empty hints but we're connecting - prompting for password")
|
||||||
switch settingName {
|
fields = []string{"password"}
|
||||||
case "802-11-wireless-security":
|
} else {
|
||||||
if wifiSecSettings, ok := conn["802-11-wireless-security"]; ok {
|
log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc)
|
||||||
if flagsVariant, ok := wifiSecSettings["psk-flags"]; ok {
|
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
||||||
if pwdFlags, ok := flagsVariant.Value().(uint32); ok {
|
|
||||||
passwordFlags = pwdFlags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "802-1x":
|
|
||||||
if dot1xSettings, ok := conn["802-1x"]; ok {
|
|
||||||
if flagsVariant, ok := dot1xSettings["password-flags"]; ok {
|
|
||||||
if pwdFlags, ok := flagsVariant.Value().(uint32); ok {
|
|
||||||
passwordFlags = pwdFlags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if passwordFlags == 0xFFFF {
|
if len(fields) == 0 {
|
||||||
log.Warnf("[SecretAgent] Could not determine password-flags for empty hints - returning NoSecrets error")
|
const (
|
||||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
NM_SETTING_SECRET_FLAG_NONE = 0
|
||||||
} else if passwordFlags&NM_SETTING_SECRET_FLAG_NOT_REQUIRED != 0 {
|
NM_SETTING_SECRET_FLAG_AGENT_OWNED = 1
|
||||||
log.Infof("[SecretAgent] Secrets not required (flags=%d)", passwordFlags)
|
NM_SETTING_SECRET_FLAG_NOT_SAVED = 2
|
||||||
out := nmSettingMap{}
|
NM_SETTING_SECRET_FLAG_NOT_REQUIRED = 4
|
||||||
out[settingName] = nmVariantMap{}
|
)
|
||||||
return out, nil
|
|
||||||
} else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 {
|
var passwordFlags uint32 = 0xFFFF
|
||||||
log.Warnf("[SecretAgent] Secrets are agent-owned but we don't store secrets (flags=%d) - returning NoSecrets error", passwordFlags)
|
switch settingName {
|
||||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
case "802-11-wireless-security":
|
||||||
} else {
|
if wifiSecSettings, ok := conn["802-11-wireless-security"]; ok {
|
||||||
log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags)
|
if flagsVariant, ok := wifiSecSettings["psk-flags"]; ok {
|
||||||
out := nmSettingMap{}
|
if pwdFlags, ok := flagsVariant.Value().(uint32); ok {
|
||||||
out[settingName] = nmVariantMap{}
|
passwordFlags = pwdFlags
|
||||||
return out, nil
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "802-1x":
|
||||||
|
if dot1xSettings, ok := conn["802-1x"]; ok {
|
||||||
|
if flagsVariant, ok := dot1xSettings["password-flags"]; ok {
|
||||||
|
if pwdFlags, ok := flagsVariant.Value().(uint32); ok {
|
||||||
|
passwordFlags = pwdFlags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if passwordFlags == 0xFFFF {
|
||||||
|
log.Warnf("[SecretAgent] Could not determine password-flags for empty hints - returning NoSecrets error")
|
||||||
|
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
||||||
|
} else if passwordFlags&NM_SETTING_SECRET_FLAG_NOT_REQUIRED != 0 {
|
||||||
|
log.Infof("[SecretAgent] Secrets not required (flags=%d)", passwordFlags)
|
||||||
|
out := nmSettingMap{}
|
||||||
|
out[settingName] = nmVariantMap{}
|
||||||
|
return out, nil
|
||||||
|
} else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 {
|
||||||
|
log.Warnf("[SecretAgent] Secrets are agent-owned but we don't store secrets (flags=%d) - returning NoSecrets error", passwordFlags)
|
||||||
|
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
||||||
|
} else {
|
||||||
|
log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags)
|
||||||
|
out := nmSettingMap{}
|
||||||
|
out[settingName] = nmVariantMap{}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,13 +358,11 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
// Update settings based on type
|
// Update settings based on type
|
||||||
switch settingName {
|
switch settingName {
|
||||||
case "vpn":
|
case "vpn":
|
||||||
// Set password-flags=0 and add secrets to vpn section
|
|
||||||
vpn, ok := existingSettings["vpn"]
|
vpn, ok := existingSettings["vpn"]
|
||||||
if !ok {
|
if !ok {
|
||||||
vpn = make(map[string]dbus.Variant)
|
vpn = make(map[string]dbus.Variant)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get existing data map (vpn.data is string->string)
|
|
||||||
var data map[string]string
|
var data map[string]string
|
||||||
if dataVariant, ok := vpn["data"]; ok {
|
if dataVariant, ok := vpn["data"]; ok {
|
||||||
if dm, ok := dataVariant.Value().(map[string]string); ok {
|
if dm, ok := dataVariant.Value().(map[string]string); ok {
|
||||||
@@ -364,11 +377,9 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
data = make(map[string]string)
|
data = make(map[string]string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update password-flags to 0 (system-stored)
|
|
||||||
data["password-flags"] = "0"
|
data["password-flags"] = "0"
|
||||||
vpn["data"] = dbus.MakeVariant(data)
|
vpn["data"] = dbus.MakeVariant(data)
|
||||||
|
|
||||||
// Add secrets (vpn.secrets is string->string)
|
|
||||||
secs := make(map[string]string)
|
secs := make(map[string]string)
|
||||||
for k, v := range reply.Secrets {
|
for k, v := range reply.Secrets {
|
||||||
secs[k] = v
|
secs[k] = v
|
||||||
@@ -379,14 +390,12 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
log.Infof("[SecretAgent] Updated VPN settings: password-flags=0, secrets with %d fields", len(secs))
|
log.Infof("[SecretAgent] Updated VPN settings: password-flags=0, secrets with %d fields", len(secs))
|
||||||
|
|
||||||
case "802-11-wireless-security":
|
case "802-11-wireless-security":
|
||||||
// Set psk-flags=0 for WiFi
|
|
||||||
wifiSec, ok := existingSettings["802-11-wireless-security"]
|
wifiSec, ok := existingSettings["802-11-wireless-security"]
|
||||||
if !ok {
|
if !ok {
|
||||||
wifiSec = make(map[string]dbus.Variant)
|
wifiSec = make(map[string]dbus.Variant)
|
||||||
}
|
}
|
||||||
wifiSec["psk-flags"] = dbus.MakeVariant(uint32(0))
|
wifiSec["psk-flags"] = dbus.MakeVariant(uint32(0))
|
||||||
|
|
||||||
// Add PSK secret
|
|
||||||
if psk, ok := reply.Secrets["psk"]; ok {
|
if psk, ok := reply.Secrets["psk"]; ok {
|
||||||
wifiSec["psk"] = dbus.MakeVariant(psk)
|
wifiSec["psk"] = dbus.MakeVariant(psk)
|
||||||
log.Infof("[SecretAgent] Updated WiFi settings: psk-flags=0")
|
log.Infof("[SecretAgent] Updated WiFi settings: psk-flags=0")
|
||||||
@@ -394,14 +403,12 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
settings["802-11-wireless-security"] = wifiSec
|
settings["802-11-wireless-security"] = wifiSec
|
||||||
|
|
||||||
case "802-1x":
|
case "802-1x":
|
||||||
// Set password-flags=0 for 802.1x
|
|
||||||
dot1x, ok := existingSettings["802-1x"]
|
dot1x, ok := existingSettings["802-1x"]
|
||||||
if !ok {
|
if !ok {
|
||||||
dot1x = make(map[string]dbus.Variant)
|
dot1x = make(map[string]dbus.Variant)
|
||||||
}
|
}
|
||||||
dot1x["password-flags"] = dbus.MakeVariant(uint32(0))
|
dot1x["password-flags"] = dbus.MakeVariant(uint32(0))
|
||||||
|
|
||||||
// Add password secret
|
|
||||||
if password, ok := reply.Secrets["password"]; ok {
|
if password, ok := reply.Secrets["password"]; ok {
|
||||||
dot1x["password"] = dbus.MakeVariant(password)
|
dot1x["password"] = dbus.MakeVariant(password)
|
||||||
log.Infof("[SecretAgent] Updated 802.1x settings: password-flags=0")
|
log.Infof("[SecretAgent] Updated 802.1x settings: password-flags=0")
|
||||||
@@ -507,6 +514,39 @@ func fieldsNeeded(setting string, hints []string) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readVPNPasswordFlags(conn map[string]nmVariantMap, settingName string) uint32 {
|
||||||
|
if settingName != "vpn" {
|
||||||
|
return 0xFFFF
|
||||||
|
}
|
||||||
|
|
||||||
|
vpnSettings, ok := conn["vpn"]
|
||||||
|
if !ok {
|
||||||
|
return 0xFFFF
|
||||||
|
}
|
||||||
|
|
||||||
|
dataVariant, ok := vpnSettings["data"]
|
||||||
|
if !ok {
|
||||||
|
return 0xFFFF
|
||||||
|
}
|
||||||
|
|
||||||
|
dataMap, ok := dataVariant.Value().(map[string]string)
|
||||||
|
if !ok {
|
||||||
|
return 0xFFFF
|
||||||
|
}
|
||||||
|
|
||||||
|
flagsStr, ok := dataMap["password-flags"]
|
||||||
|
if !ok {
|
||||||
|
return 0xFFFF
|
||||||
|
}
|
||||||
|
|
||||||
|
flags64, err := strconv.ParseUint(flagsStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0xFFFF
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint32(flags64)
|
||||||
|
}
|
||||||
|
|
||||||
func reasonFromFlags(flags uint32) string {
|
func reasonFromFlags(flags uint32) string {
|
||||||
const (
|
const (
|
||||||
NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE = 0x0
|
NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE = 0x0
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||||
activeConn, err := nm.ActivateConnection(targetConn, nil, nil)
|
_, err = nm.ActivateConnection(targetConn, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.stateMutex.Lock()
|
b.stateMutex.Lock()
|
||||||
b.state.IsConnectingVPN = false
|
b.state.IsConnectingVPN = false
|
||||||
@@ -249,20 +249,6 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
|||||||
return fmt.Errorf("failed to activate VPN: %w", err)
|
return fmt.Errorf("failed to activate VPN: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if activeConn != nil {
|
|
||||||
state, _ := activeConn.GetPropertyState()
|
|
||||||
if state == 2 {
|
|
||||||
b.stateMutex.Lock()
|
|
||||||
b.state.IsConnectingVPN = false
|
|
||||||
b.state.ConnectingVPNUUID = ""
|
|
||||||
b.stateMutex.Unlock()
|
|
||||||
b.ListActiveVPN()
|
|
||||||
if b.onStateChange != nil {
|
|
||||||
b.onStateChange()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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, ¤tState) {
|
if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, ¤tState) {
|
||||||
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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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, ¤tState) {
|
if m.lastNotified != nil && !stateChanged(m.lastNotified, ¤tState) {
|
||||||
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()
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Keep this sorted
|
||||||
|
|
||||||
|
rajveermalviya
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Wayland implementation in Go
|
||||||
|
|
||||||
|
[](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
|
||||||
|
```
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cd ./wayland
|
||||||
|
go generate -x ./...
|
||||||
Executable
+9
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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(©Read)
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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" {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,9 @@ Singleton {
|
|||||||
if (!modal.allowStacking) {
|
if (!modal.allowStacking) {
|
||||||
closeAllModalsExcept(modal)
|
closeAllModalsExcept(modal)
|
||||||
}
|
}
|
||||||
|
if (!modal.keepPopoutsOpen) {
|
||||||
|
PopoutManager.closeAllPopouts()
|
||||||
|
}
|
||||||
|
TrayMenuManager.closeAllMenus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ Singleton {
|
|||||||
Top,
|
Top,
|
||||||
Bottom,
|
Bottom,
|
||||||
Left,
|
Left,
|
||||||
Right
|
Right,
|
||||||
|
TopCenter,
|
||||||
|
BottomCenter,
|
||||||
|
LeftCenter,
|
||||||
|
RightCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AnimationSpeed {
|
enum AnimationSpeed {
|
||||||
@@ -40,6 +44,11 @@ Singleton {
|
|||||||
SuspendThenHibernate
|
SuspendThenHibernate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum WidgetColorMode {
|
||||||
|
Default,
|
||||||
|
Colorful
|
||||||
|
}
|
||||||
|
|
||||||
readonly property string defaultFontFamily: "Inter Variable"
|
readonly property string defaultFontFamily: "Inter Variable"
|
||||||
readonly property string defaultMonoFontFamily: "Fira Code"
|
readonly property string defaultMonoFontFamily: "Fira Code"
|
||||||
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||||
@@ -66,6 +75,7 @@ Singleton {
|
|||||||
property real popupTransparency: 1.0
|
property real popupTransparency: 1.0
|
||||||
property real dockTransparency: 1
|
property real dockTransparency: 1
|
||||||
property string widgetBackgroundColor: "sch"
|
property string widgetBackgroundColor: "sch"
|
||||||
|
property string widgetColorMode: "default"
|
||||||
property real cornerRadius: 12
|
property real cornerRadius: 12
|
||||||
|
|
||||||
property bool use24HourClock: true
|
property bool use24HourClock: true
|
||||||
@@ -167,6 +177,7 @@ Singleton {
|
|||||||
property string spotlightModalViewMode: "list"
|
property string spotlightModalViewMode: "list"
|
||||||
property bool sortAppsAlphabetically: false
|
property bool sortAppsAlphabetically: false
|
||||||
property int appLauncherGridColumns: 4
|
property int appLauncherGridColumns: 4
|
||||||
|
property bool spotlightCloseNiriOverview: true
|
||||||
|
|
||||||
property string weatherLocation: "New York, NY"
|
property string weatherLocation: "New York, NY"
|
||||||
property string weatherCoordinates: "40.7128,-74.0060"
|
property string weatherCoordinates: "40.7128,-74.0060"
|
||||||
@@ -305,6 +316,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 +339,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 +603,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
// Separated from Theme.qml to keep that file clean
|
// Separated from Theme.qml to keep that file clean
|
||||||
|
|
||||||
const CatppuccinMocha = {
|
const CatppuccinMocha = {
|
||||||
surface: "#313244",
|
surface: "#181825",
|
||||||
surfaceText: "#cdd6f4",
|
surfaceText: "#cdd6f4",
|
||||||
surfaceVariant: "#313244",
|
surfaceVariant: "#1e1e2e",
|
||||||
surfaceVariantText: "#a6adc8",
|
surfaceVariantText: "#a6adc8",
|
||||||
background: "#1e1e2e",
|
background: "#181825",
|
||||||
backgroundText: "#cdd6f4",
|
backgroundText: "#cdd6f4",
|
||||||
outline: "#6c7086",
|
outline: "#6c7086",
|
||||||
surfaceContainer: "#45475a",
|
surfaceContainer: "#1e1e2e",
|
||||||
surfaceContainerHigh: "#585b70",
|
surfaceContainerHigh: "#313244",
|
||||||
surfaceContainerHighest: "#6c7086"
|
surfaceContainerHighest: "#45475a"
|
||||||
}
|
}
|
||||||
|
|
||||||
const CatppuccinLatte = {
|
const CatppuccinLatte = {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -636,6 +665,34 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property color widgetIconColor: {
|
||||||
|
if (typeof SettingsData === "undefined") {
|
||||||
|
return surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (SettingsData.widgetColorMode) {
|
||||||
|
case "colorful":
|
||||||
|
return surfaceText
|
||||||
|
case "default":
|
||||||
|
default:
|
||||||
|
return surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property color widgetTextColor: {
|
||||||
|
if (typeof SettingsData === "undefined") {
|
||||||
|
return surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (SettingsData.widgetColorMode) {
|
||||||
|
case "colorful":
|
||||||
|
return primary
|
||||||
|
case "default":
|
||||||
|
default:
|
||||||
|
return surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function isColorDark(c) {
|
function isColorDark(c) {
|
||||||
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5
|
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5
|
||||||
@@ -650,10 +707,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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ var SPEC = {
|
|||||||
dockTransparency: { def: 1.0, coerce: percentToUnit },
|
dockTransparency: { def: 1.0, coerce: percentToUnit },
|
||||||
|
|
||||||
widgetBackgroundColor: { def: "sch" },
|
widgetBackgroundColor: { def: "sch" },
|
||||||
|
widgetColorMode: { def: "default" },
|
||||||
cornerRadius: { def: 12, onChange: "updateNiriLayout" },
|
cornerRadius: { def: 12, onChange: "updateNiriLayout" },
|
||||||
|
|
||||||
use24HourClock: { def: true },
|
use24HourClock: { def: true },
|
||||||
@@ -97,6 +98,7 @@ var SPEC = {
|
|||||||
spotlightModalViewMode: { def: "list" },
|
spotlightModalViewMode: { def: "list" },
|
||||||
sortAppsAlphabetically: { def: false },
|
sortAppsAlphabetically: { def: false },
|
||||||
appLauncherGridColumns: { def: 4 },
|
appLauncherGridColumns: { def: 4 },
|
||||||
|
spotlightCloseNiriOverview: { def: true },
|
||||||
|
|
||||||
weatherLocation: { def: "New York, NY" },
|
weatherLocation: { def: "New York, NY" },
|
||||||
weatherCoordinates: { def: "40.7128,-74.0060" },
|
weatherCoordinates: { def: "40.7128,-74.0060" },
|
||||||
@@ -215,6 +217,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 +240,7 @@ var SPEC = {
|
|||||||
updaterCustomCommand: { def: "" },
|
updaterCustomCommand: { def: "" },
|
||||||
updaterTerminalAdditionalParams: { def: "" },
|
updaterTerminalAdditionalParams: { def: "" },
|
||||||
|
|
||||||
|
displayNameMode: { def: "system" },
|
||||||
screenPreferences: { def: {} },
|
screenPreferences: { def: {} },
|
||||||
showOnLastDisplay: { def: {} }
|
showOnLastDisplay: { def: {} }
|
||||||
};
|
};
|
||||||
|
|||||||
+17
-43
@@ -22,7 +22,7 @@ import qs.Modules.ProcessList
|
|||||||
import qs.Modules.Settings
|
import qs.Modules.Settings
|
||||||
import qs.Modules.DankBar
|
import qs.Modules.DankBar
|
||||||
import qs.Modules.DankBar.Popouts
|
import qs.Modules.DankBar.Popouts
|
||||||
import qs.Modules.HyprWorkspaces
|
import qs.Modules.WorkspaceOverlays
|
||||||
import qs.Modules.Plugins
|
import qs.Modules.Plugins
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -615,4 +581,12 @@ Item {
|
|||||||
id: hyprlandOverview
|
id: hyprlandOverview
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: niriOverviewOverlayLoader
|
||||||
|
active: CompositorService.isNiri
|
||||||
|
component: NiriOverviewOverlay {
|
||||||
|
id: niriOverviewOverlay
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: lockScreenSection.implicitHeight + Theme.spacingL * 2
|
height: lockScreenSection.implicitHeight + Theme.spacingL * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
color: Theme.surfaceContainerHigh
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: timeoutSection.implicitHeight + Theme.spacingL * 2
|
height: timeoutSection.implicitHeight + Theme.spacingL * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
color: Theme.surfaceContainerHigh
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: powerMenuCustomSection.implicitHeight + Theme.spacingL * 2
|
height: powerMenuCustomSection.implicitHeight + Theme.spacingL * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
color: Theme.surfaceContainerHigh
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
@@ -516,7 +516,7 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: powerCommandConfirmSection.implicitHeight + Theme.spacingL * 2
|
height: powerCommandConfirmSection.implicitHeight + Theme.spacingL * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
color: Theme.surfaceContainerHigh
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
@@ -560,7 +560,7 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: powerCommandCustomization.implicitHeight + Theme.spacingL * 2
|
height: powerCommandCustomization.implicitHeight + Theme.spacingL * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
color: Theme.surfaceContainerHigh
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user