1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-14 09:42:10 -04:00

miraclewm: add support for Miracle WM

This commit is contained in:
bbedward
2026-02-16 23:00:07 -05:00
parent d62bdda56b
commit 0b33d3f905
26 changed files with 1889 additions and 163 deletions

View File

@@ -19,7 +19,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
</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), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), 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/), [Scroll](https://github.com/dawsers/scroll), [Miracle WM](https://github.com/miracle-wm-org/miracle-wm), 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/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), and [Scroll](https://github.com/dawsers/scroll) 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), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and [Miracle WM](https://github.com/miracle-wm-org/miracle-wm) 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)

View File

@@ -97,6 +97,11 @@ func initializeProviders() {
log.Warnf("Failed to register Scroll provider: %v", err) log.Warnf("Failed to register Scroll provider: %v", err)
} }
miracleProvider := providers.NewMiracleProvider("$HOME/.config/miracle-wm")
if err := registry.Register(miracleProvider); err != nil {
log.Warnf("Failed to register Miracle WM provider: %v", err)
}
swayProvider := providers.NewSwayProvider("$HOME/.config/sway") swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
if err := registry.Register(swayProvider); err != nil { if err := registry.Register(swayProvider); err != nil {
log.Warnf("Failed to register Sway provider: %v", err) log.Warnf("Failed to register Sway provider: %v", err)
@@ -144,6 +149,8 @@ func makeProviderWithPath(name, path string) keybinds.Provider {
return providers.NewSwayProvider(path) return providers.NewSwayProvider(path)
case "scroll": case "scroll":
return providers.NewSwayProvider(path) return providers.NewSwayProvider(path)
case "miracle":
return providers.NewMiracleProvider(path)
case "niri": case "niri":
return providers.NewNiriProvider(path) return providers.NewNiriProvider(path)
default: default:

View File

@@ -19,7 +19,7 @@ require (
github.com/yuin/goldmark v1.7.16 github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20260211191109-2735e65f0518 golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
golang.org/x/image v0.36.0 golang.org/x/image v0.36.0
) )
@@ -27,7 +27,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
@@ -36,7 +36,7 @@ require (
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
@@ -55,7 +55,7 @@ require (
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-20260210102253-e4d10f0e569a github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
@@ -71,7 +71,7 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.41.0 golang.org/x/sys v0.41.0
golang.org/x/text v0.34.0 golang.org/x/text v0.34.0
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1
) )
// v0.0.1 tag is missing a LICENSE file; master has it. // v0.0.1 tag is missing a LICENSE file; master has it.

View File

@@ -40,8 +40,8 @@ github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSg
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -68,8 +68,8 @@ github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g8
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc= github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII= github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw= github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
github.com/go-git/go-git/v6 v6.0.0-20260210102253-e4d10f0e569a h1:LLju0NuXQqR4WmGl1Dm86b9ZXsvXgLYbx/aaAjdQr6w= github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s=
github.com/go-git/go-git/v6 v6.0.0-20260210102253-e4d10f0e569a/go.mod h1:IdXOePSwsMKGpuAbpczsm+f0Uy5fdHHjwgDPOymKA78= github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -86,8 +86,8 @@ github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvE
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk= github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -152,8 +152,8 @@ go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260211191109-2735e65f0518 h1:2E1CW7v5QB+Wi3N+MXllOtVR6SFmI8iJM8EdzgxrgrU= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
golang.org/x/exp v0.0.0-20260211191109-2735e65f0518/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=

View File

@@ -0,0 +1,97 @@
package providers
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
type MiracleProvider struct {
configPath string
}
func NewMiracleProvider(configPath string) *MiracleProvider {
if configPath == "" {
configDir, err := os.UserConfigDir()
if err == nil {
configPath = filepath.Join(configDir, "miracle-wm")
} else {
configPath = "$HOME/.config/miracle-wm"
}
}
return &MiracleProvider{configPath: configPath}
}
func (m *MiracleProvider) Name() string {
return "miracle"
}
func (m *MiracleProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
config, err := ParseMiracleConfig(m.configPath)
if err != nil {
return nil, fmt.Errorf("failed to parse miracle-wm config: %w", err)
}
bindings := MiracleConfigToBindings(config)
categorizedBinds := make(map[string][]keybinds.Keybind)
for _, kb := range bindings {
category := m.categorizeAction(kb.Action)
bind := keybinds.Keybind{
Key: m.formatKey(kb),
Description: kb.Comment,
Action: kb.Action,
}
categorizedBinds[category] = append(categorizedBinds[category], bind)
}
return &keybinds.CheatSheet{
Title: "Miracle WM Keybinds",
Provider: m.Name(),
Binds: categorizedBinds,
}, nil
}
func (m *MiracleProvider) GetOverridePath() string {
expanded, err := utils.ExpandPath(m.configPath)
if err != nil {
return filepath.Join(m.configPath, "config.yaml")
}
return filepath.Join(expanded, "config.yaml")
}
func (m *MiracleProvider) formatKey(kb MiracleKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}
func (m *MiracleProvider) categorizeAction(action string) string {
switch {
case strings.HasPrefix(action, "select_workspace_") || strings.HasPrefix(action, "move_to_workspace_"):
return "Workspace"
case strings.Contains(action, "select_") || strings.Contains(action, "move_"):
return "Window"
case action == "toggle_resize" || strings.HasPrefix(action, "resize_"):
return "Window"
case action == "fullscreen" || action == "toggle_floating" || action == "quit_active_window" || action == "toggle_pinned_to_workspace":
return "Window"
case action == "toggle_tabbing" || action == "toggle_stacking" || action == "request_vertical" || action == "request_horizontal":
return "Layout"
case action == "quit_compositor":
return "System"
case action == "terminal":
return "Execute"
case strings.HasPrefix(action, "magnifier_"):
return "Accessibility"
case strings.HasPrefix(action, "dms ") || strings.Contains(action, "dms ipc"):
return "Execute"
default:
return "Execute"
}
}

View File

@@ -0,0 +1,320 @@
package providers
import (
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"gopkg.in/yaml.v3"
)
type MiracleConfig struct {
Terminal string `yaml:"terminal"`
ActionKey string `yaml:"action_key"`
DefaultActionOverrides []MiracleActionOverride `yaml:"default_action_overrides"`
CustomActions []MiracleCustomAction `yaml:"custom_actions"`
}
type MiracleActionOverride struct {
Name string `yaml:"name"`
Action string `yaml:"action"`
Modifiers []string `yaml:"modifiers"`
Key string `yaml:"key"`
}
type MiracleCustomAction struct {
Command string `yaml:"command"`
Action string `yaml:"action"`
Modifiers []string `yaml:"modifiers"`
Key string `yaml:"key"`
}
type MiracleKeyBinding struct {
Mods []string
Key string
Action string
Comment string
}
var miracleDefaultBinds = []MiracleKeyBinding{
{Mods: []string{"Super"}, Key: "Return", Action: "terminal", Comment: "Open terminal"},
{Mods: []string{"Super"}, Key: "v", Action: "request_vertical", Comment: "Layout windows vertically"},
{Mods: []string{"Super"}, Key: "h", Action: "request_horizontal", Comment: "Layout windows horizontally"},
{Mods: []string{"Super"}, Key: "Up", Action: "select_up", Comment: "Select window above"},
{Mods: []string{"Super"}, Key: "Down", Action: "select_down", Comment: "Select window below"},
{Mods: []string{"Super"}, Key: "Left", Action: "select_left", Comment: "Select window left"},
{Mods: []string{"Super"}, Key: "Right", Action: "select_right", Comment: "Select window right"},
{Mods: []string{"Super", "Shift"}, Key: "Up", Action: "move_up", Comment: "Move window up"},
{Mods: []string{"Super", "Shift"}, Key: "Down", Action: "move_down", Comment: "Move window down"},
{Mods: []string{"Super", "Shift"}, Key: "Left", Action: "move_left", Comment: "Move window left"},
{Mods: []string{"Super", "Shift"}, Key: "Right", Action: "move_right", Comment: "Move window right"},
{Mods: []string{"Super"}, Key: "r", Action: "toggle_resize", Comment: "Toggle resize mode"},
{Mods: []string{"Super"}, Key: "f", Action: "fullscreen", Comment: "Toggle fullscreen"},
{Mods: []string{"Super", "Shift"}, Key: "q", Action: "quit_active_window", Comment: "Close window"},
{Mods: []string{"Super", "Shift"}, Key: "e", Action: "quit_compositor", Comment: "Exit compositor"},
{Mods: []string{"Super"}, Key: "Space", Action: "toggle_floating", Comment: "Toggle floating"},
{Mods: []string{"Super", "Shift"}, Key: "p", Action: "toggle_pinned_to_workspace", Comment: "Toggle pinned to workspace"},
{Mods: []string{"Super"}, Key: "w", Action: "toggle_tabbing", Comment: "Toggle tabbing layout"},
{Mods: []string{"Super"}, Key: "s", Action: "toggle_stacking", Comment: "Toggle stacking layout"},
{Mods: []string{"Super"}, Key: "1", Action: "select_workspace_0", Comment: "Workspace 1"},
{Mods: []string{"Super"}, Key: "2", Action: "select_workspace_1", Comment: "Workspace 2"},
{Mods: []string{"Super"}, Key: "3", Action: "select_workspace_2", Comment: "Workspace 3"},
{Mods: []string{"Super"}, Key: "4", Action: "select_workspace_3", Comment: "Workspace 4"},
{Mods: []string{"Super"}, Key: "5", Action: "select_workspace_4", Comment: "Workspace 5"},
{Mods: []string{"Super"}, Key: "6", Action: "select_workspace_5", Comment: "Workspace 6"},
{Mods: []string{"Super"}, Key: "7", Action: "select_workspace_6", Comment: "Workspace 7"},
{Mods: []string{"Super"}, Key: "8", Action: "select_workspace_7", Comment: "Workspace 8"},
{Mods: []string{"Super"}, Key: "9", Action: "select_workspace_8", Comment: "Workspace 9"},
{Mods: []string{"Super"}, Key: "0", Action: "select_workspace_9", Comment: "Workspace 10"},
{Mods: []string{"Super", "Shift"}, Key: "1", Action: "move_to_workspace_0", Comment: "Move to workspace 1"},
{Mods: []string{"Super", "Shift"}, Key: "2", Action: "move_to_workspace_1", Comment: "Move to workspace 2"},
{Mods: []string{"Super", "Shift"}, Key: "3", Action: "move_to_workspace_2", Comment: "Move to workspace 3"},
{Mods: []string{"Super", "Shift"}, Key: "4", Action: "move_to_workspace_3", Comment: "Move to workspace 4"},
{Mods: []string{"Super", "Shift"}, Key: "5", Action: "move_to_workspace_4", Comment: "Move to workspace 5"},
{Mods: []string{"Super", "Shift"}, Key: "6", Action: "move_to_workspace_5", Comment: "Move to workspace 6"},
{Mods: []string{"Super", "Shift"}, Key: "7", Action: "move_to_workspace_6", Comment: "Move to workspace 7"},
{Mods: []string{"Super", "Shift"}, Key: "8", Action: "move_to_workspace_7", Comment: "Move to workspace 8"},
{Mods: []string{"Super", "Shift"}, Key: "9", Action: "move_to_workspace_8", Comment: "Move to workspace 9"},
{Mods: []string{"Super", "Shift"}, Key: "0", Action: "move_to_workspace_9", Comment: "Move to workspace 10"},
}
func ParseMiracleConfig(configPath string) (*MiracleConfig, error) {
expanded, err := utils.ExpandPath(configPath)
if err != nil {
return nil, err
}
info, err := os.Stat(expanded)
if err != nil {
return nil, err
}
var configFile string
if info.IsDir() {
configFile = filepath.Join(expanded, "config.yaml")
} else {
configFile = expanded
}
data, err := os.ReadFile(configFile)
if err != nil {
return nil, err
}
var config MiracleConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
if config.ActionKey == "" {
config.ActionKey = "meta"
}
return &config, nil
}
func resolveMiracleModifier(mod, actionKey string) string {
switch mod {
case "primary":
return resolveActionKey(actionKey)
case "alt", "alt_left", "alt_right":
return "Alt"
case "shift", "shift_left", "shift_right":
return "Shift"
case "ctrl", "ctrl_left", "ctrl_right":
return "Ctrl"
case "meta", "meta_left", "meta_right":
return "Super"
default:
return mod
}
}
func resolveActionKey(actionKey string) string {
switch actionKey {
case "meta":
return "Super"
case "alt":
return "Alt"
case "ctrl":
return "Ctrl"
default:
return "Super"
}
}
func miracleKeyCodeToName(keyCode string) string {
name := strings.TrimPrefix(keyCode, "KEY_")
name = strings.ToLower(name)
switch name {
case "enter":
return "Return"
case "space":
return "Space"
case "up":
return "Up"
case "down":
return "Down"
case "left":
return "Left"
case "right":
return "Right"
case "tab":
return "Tab"
case "escape", "esc":
return "Escape"
case "delete":
return "Delete"
case "backspace":
return "BackSpace"
case "home":
return "Home"
case "end":
return "End"
case "pageup":
return "Page_Up"
case "pagedown":
return "Page_Down"
case "print":
return "Print"
case "pause":
return "Pause"
case "volumeup":
return "XF86AudioRaiseVolume"
case "volumedown":
return "XF86AudioLowerVolume"
case "mute":
return "XF86AudioMute"
case "micmute":
return "XF86AudioMicMute"
case "brightnessup":
return "XF86MonBrightnessUp"
case "brightnessdown":
return "XF86MonBrightnessDown"
case "kbdillumup":
return "XF86KbdBrightnessUp"
case "kbdillumdown":
return "XF86KbdBrightnessDown"
case "comma":
return "comma"
case "minus":
return "minus"
case "equal":
return "equal"
}
if len(name) == 1 {
return name
}
return name
}
func MiracleConfigToBindings(config *MiracleConfig) []MiracleKeyBinding {
overridden := make(map[string]bool)
var bindings []MiracleKeyBinding
for _, override := range config.DefaultActionOverrides {
mods := make([]string, 0, len(override.Modifiers))
for _, mod := range override.Modifiers {
mods = append(mods, resolveMiracleModifier(mod, config.ActionKey))
}
bindings = append(bindings, MiracleKeyBinding{
Mods: mods,
Key: miracleKeyCodeToName(override.Key),
Action: override.Name,
Comment: miracleActionDescription(override.Name),
})
overridden[override.Name] = true
}
for _, def := range miracleDefaultBinds {
if overridden[def.Action] {
continue
}
bindings = append(bindings, def)
}
for _, custom := range config.CustomActions {
mods := make([]string, 0, len(custom.Modifiers))
for _, mod := range custom.Modifiers {
mods = append(mods, resolveMiracleModifier(mod, config.ActionKey))
}
bindings = append(bindings, MiracleKeyBinding{
Mods: mods,
Key: miracleKeyCodeToName(custom.Key),
Action: custom.Command,
Comment: custom.Command,
})
}
return bindings
}
func miracleActionDescription(action string) string {
switch action {
case "terminal":
return "Open terminal"
case "request_vertical":
return "Layout windows vertically"
case "request_horizontal":
return "Layout windows horizontally"
case "select_up":
return "Select window above"
case "select_down":
return "Select window below"
case "select_left":
return "Select window left"
case "select_right":
return "Select window right"
case "move_up":
return "Move window up"
case "move_down":
return "Move window down"
case "move_left":
return "Move window left"
case "move_right":
return "Move window right"
case "toggle_resize":
return "Toggle resize mode"
case "fullscreen":
return "Toggle fullscreen"
case "quit_active_window":
return "Close window"
case "quit_compositor":
return "Exit compositor"
case "toggle_floating":
return "Toggle floating"
case "toggle_pinned_to_workspace":
return "Toggle pinned to workspace"
case "toggle_tabbing":
return "Toggle tabbing layout"
case "toggle_stacking":
return "Toggle stacking layout"
case "magnifier_on":
return "Enable magnifier"
case "magnifier_off":
return "Disable magnifier"
case "magnifier_increase_size":
return "Increase magnifier area"
case "magnifier_decrease_size":
return "Decrease magnifier area"
case "magnifier_increase_scale":
return "Increase magnifier scale"
case "magnifier_decrease_scale":
return "Decrease magnifier scale"
}
if num, ok := strings.CutPrefix(action, "select_workspace_"); ok {
return "Workspace " + num
}
if num, ok := strings.CutPrefix(action, "move_to_workspace_"); ok {
return "Move to workspace " + num
}
return action
}

View File

@@ -25,7 +25,6 @@ func NewSwayProvider(configPath string) *SwayProvider {
configPath = "$HOME/.config/sway" configPath = "$HOME/.config/sway"
} }
} else { } else {
// Determine isScroll based on the provided config path
isScroll = strings.Contains(configPath, "scroll") isScroll = strings.Contains(configPath, "scroll")
} }
@@ -36,16 +35,16 @@ func NewSwayProvider(configPath string) *SwayProvider {
} }
func (s *SwayProvider) Name() string { func (s *SwayProvider) Name() string {
if s != nil && s.isScroll {
return "scroll"
}
if s == nil { if s == nil {
_, ok := os.LookupEnv("SCROLLSOCK") if os.Getenv("SCROLLSOCK") != "" {
if ok {
return "scroll" return "scroll"
} }
return "sway"
} }
if s.isScroll {
return "scroll"
}
return "sway" return "sway"
} }

View File

@@ -21,6 +21,7 @@ const (
CompositorNiri CompositorNiri
CompositorDWL CompositorDWL
CompositorScroll CompositorScroll
CompositorMiracle
) )
var detectedCompositor Compositor = -1 var detectedCompositor Compositor = -1
@@ -34,6 +35,7 @@ func DetectCompositor() Compositor {
niriSocket := os.Getenv("NIRI_SOCKET") niriSocket := os.Getenv("NIRI_SOCKET")
swaySocket := os.Getenv("SWAYSOCK") swaySocket := os.Getenv("SWAYSOCK")
scrollSocket := os.Getenv("SCROLLSOCK") scrollSocket := os.Getenv("SCROLLSOCK")
miracleSocket := os.Getenv("MIRACLESOCK")
switch { switch {
case niriSocket != "": case niriSocket != "":
@@ -46,7 +48,11 @@ func DetectCompositor() Compositor {
detectedCompositor = CompositorScroll detectedCompositor = CompositorScroll
return detectedCompositor return detectedCompositor
} }
case miracleSocket != "":
if _, err := os.Stat(miracleSocket); err == nil {
detectedCompositor = CompositorMiracle
return detectedCompositor
}
case swaySocket != "": case swaySocket != "":
if _, err := os.Stat(swaySocket); err == nil { if _, err := os.Stat(swaySocket); err == nil {
detectedCompositor = CompositorSway detectedCompositor = CompositorSway
@@ -260,6 +266,25 @@ func getScrollFocusedMonitor() string {
return "" return ""
} }
func getMiracleFocusedMonitor() string {
output, err := exec.Command("miraclemsg", "-t", "get_workspaces").Output()
if err != nil {
return ""
}
var workspaces []swayWorkspace
if err := json.Unmarshal(output, &workspaces); err != nil {
return ""
}
for _, ws := range workspaces {
if ws.Focused {
return ws.Output
}
}
return ""
}
type niriWorkspace struct { type niriWorkspace struct {
Output string `json:"output"` Output string `json:"output"`
IsFocused bool `json:"is_focused"` IsFocused bool `json:"is_focused"`
@@ -407,6 +432,8 @@ func GetFocusedMonitor() string {
return getSwayFocusedMonitor() return getSwayFocusedMonitor()
case CompositorScroll: case CompositorScroll:
return getScrollFocusedMonitor() return getScrollFocusedMonitor()
case CompositorMiracle:
return getMiracleFocusedMonitor()
case CompositorNiri: case CompositorNiri:
return getNiriFocusedMonitor() return getNiriFocusedMonitor()
case CompositorDWL: case CompositorDWL:

View File

@@ -73,6 +73,7 @@ in
"labwc" "labwc"
"mango" "mango"
"scroll" "scroll"
"miracle"
]; ];
description = "Compositor to run greeter in"; description = "Compositor to run greeter in";
}; };

View File

@@ -197,7 +197,7 @@ Item {
if (CompositorService.isNiri && NiriService.currentOutput) { if (CompositorService.isNiri && NiriService.currentOutput) {
return NiriService.currentOutput; return NiriService.currentOutput;
} }
if ((CompositorService.isSway || CompositorService.isScroll) && I3.workspaces?.values) { if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && I3.workspaces?.values) {
const focusedWs = I3.workspaces.values.find(ws => ws.focused === true); const focusedWs = I3.workspaces.values.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || ""; return focusedWs?.monitor?.name || "";
} }

View File

@@ -96,7 +96,7 @@ Item {
focusedScreenName = Hyprland.focusedWorkspace.monitor.name; focusedScreenName = Hyprland.focusedWorkspace.monitor.name;
} else if (CompositorService.isNiri && NiriService.currentOutput) { } else if (CompositorService.isNiri && NiriService.currentOutput) {
focusedScreenName = NiriService.currentOutput; focusedScreenName = NiriService.currentOutput;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
focusedScreenName = focusedWs?.monitor?.name || ""; focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) { } else if (CompositorService.isDwl && DwlService.activeOutput) {
@@ -125,7 +125,7 @@ Item {
focusedScreenName = Hyprland.focusedWorkspace.monitor.name; focusedScreenName = Hyprland.focusedWorkspace.monitor.name;
} else if (CompositorService.isNiri && NiriService.currentOutput) { } else if (CompositorService.isNiri && NiriService.currentOutput) {
focusedScreenName = NiriService.currentOutput; focusedScreenName = NiriService.currentOutput;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
focusedScreenName = focusedWs?.monitor?.name || ""; focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) { } else if (CompositorService.isDwl && DwlService.activeOutput) {

View File

@@ -103,7 +103,7 @@ Item {
}, (_, i) => i); }, (_, i) => i);
} }
return DwlService.getVisibleTags(barWindow.screenName); return DwlService.getVisibleTags(barWindow.screenName);
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const workspaces = I3.workspaces?.values || []; const workspaces = I3.workspaces?.values || [];
if (workspaces.length === 0) if (workspaces.length === 0)
return [ return [
@@ -145,7 +145,7 @@ Item {
return 0; return 0;
const activeTags = DwlService.getActiveTags(barWindow.screenName); const activeTags = DwlService.getActiveTags(barWindow.screenName);
return activeTags.length > 0 ? activeTags[0] : 0; return activeTags.length > 0 ? activeTags[0] : 0;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
if (!barWindow.screenName || SettingsData.workspaceFollowFocus) { if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
return focusedWs ? focusedWs.num : 1; return focusedWs ? focusedWs.num : 1;
@@ -194,7 +194,7 @@ Item {
if (nextIndex !== validIndex) { if (nextIndex !== validIndex) {
DwlService.switchToTag(barWindow.screenName, realWorkspaces[nextIndex]); DwlService.switchToTag(barWindow.screenName, realWorkspaces[nextIndex]);
} }
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const currentWs = getCurrentWorkspace(); const currentWs = getCurrentWorkspace();
const currentIndex = realWorkspaces.findIndex(ws => ws.num === currentWs); const currentIndex = realWorkspaces.findIndex(ws => ws.num === currentWs);
const validIndex = currentIndex === -1 ? 0 : currentIndex; const validIndex = currentIndex === -1 ? 0 : currentIndex;

View File

@@ -55,7 +55,7 @@ BasePill {
} }
IconImage { IconImage {
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isLabwc) visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
anchors.centerIn: parent anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground) width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground) height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
@@ -72,6 +72,8 @@ BasePill {
return "file://" + Theme.shellDir + "/assets/sway.svg"; return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isScroll) { } else if (CompositorService.isScroll) {
return "file://" + Theme.shellDir + "/assets/sway.svg"; return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isMiracle) {
return "file://" + Theme.shellDir + "/assets/miraclewm.svg";
} else if (CompositorService.isLabwc) { } else if (CompositorService.isLabwc) {
return "file://" + Theme.shellDir + "/assets/labwc.png"; return "file://" + Theme.shellDir + "/assets/labwc.png";
} }

View File

@@ -37,6 +37,7 @@ Item {
return DwlService.activeOutput || root.screenName; return DwlService.activeOutput || root.screenName;
case "sway": case "sway":
case "scroll": case "scroll":
case "miracle":
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || root.screenName; return focusedWs?.monitor?.name || root.screenName;
default: default:
@@ -44,7 +45,7 @@ Item {
} }
} }
readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && ExtWorkspaceService.extWorkspaceAvailable) readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && !CompositorService.isMiracle && ExtWorkspaceService.extWorkspaceAvailable)
Connections { Connections {
target: DesktopEntries target: DesktopEntries
@@ -67,6 +68,7 @@ Item {
return activeTags.length > 0 ? activeTags[0] : -1; return activeTags.length > 0 ? activeTags[0] : -1;
case "sway": case "sway":
case "scroll": case "scroll":
case "miracle":
return getSwayActiveWorkspace(); return getSwayActiveWorkspace();
default: default:
return 1; return 1;
@@ -97,6 +99,7 @@ Item {
break; break;
case "sway": case "sway":
case "scroll": case "scroll":
case "miracle":
baseList = getSwayWorkspaces(); baseList = getSwayWorkspaces();
break; break;
default: default:
@@ -114,12 +117,23 @@ Item {
} }
]; ];
function mapWorkspace(ws) {
return {
"num": ws.number,
"name": ws.name,
"focused": ws.focused,
"active": ws.active,
"urgent": ws.urgent,
"monitor": ws.monitor
};
}
if (!root.screenName || SettingsData.workspaceFollowFocus) { if (!root.screenName || SettingsData.workspaceFollowFocus) {
return workspaces.slice().sort((a, b) => a.num - b.num); return workspaces.slice().sort((a, b) => a.num - b.num).map(mapWorkspace);
} }
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName); const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName);
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [ return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num).map(mapWorkspace) : [
{ {
"num": 1 "num": 1
} }
@@ -222,7 +236,7 @@ Item {
return []; return [];
} }
targetWorkspaceId = ws.tag; targetWorkspaceId = ws.tag;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
targetWorkspaceId = ws.num !== undefined ? ws.num : ws; targetWorkspaceId = ws.num !== undefined ? ws.num : ws;
} else { } else {
return []; return [];
@@ -234,7 +248,7 @@ Item {
let isActiveWs = false; let isActiveWs = false;
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active); isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active);
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false; isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false;
} else if (CompositorService.isDwl) { } else if (CompositorService.isDwl) {
@@ -255,7 +269,7 @@ Item {
let winWs = null; let winWs = null;
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
winWs = w.workspace_id; winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
winWs = w.workspace?.num; winWs = w.workspace?.num;
} else { } else {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []); const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
@@ -322,7 +336,7 @@ Item {
placeholder = { placeholder = {
"tag": -1 "tag": -1
}; };
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
placeholder = { placeholder = {
"num": -1 "num": -1
}; };
@@ -516,7 +530,7 @@ Item {
return ws && ws.id !== -1; return ws && ws.id !== -1;
if (CompositorService.isDwl) if (CompositorService.isDwl)
return ws && ws.tag !== -1; return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return ws && ws.num !== -1; return ws && ws.num !== -1;
return ws !== -1; return ws !== -1;
}); });
@@ -588,7 +602,7 @@ Item {
} }
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag); DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const realWorkspaces = getRealWorkspaces(); const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) { if (realWorkspaces.length < 2) {
return; return;
@@ -617,7 +631,7 @@ Item {
return modelData?.id || ""; return modelData?.id || "";
if (CompositorService.isDwl) if (CompositorService.isDwl)
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""; return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
if (CompositorService.isSway || CompositorService.isScroll) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return modelData?.num || ""; return modelData?.num || "";
return modelData - 1; return modelData - 1;
} }
@@ -632,7 +646,7 @@ Item {
isPlaceholder = modelData?.id === -1; isPlaceholder = modelData?.id === -1;
} else if (CompositorService.isDwl) { } else if (CompositorService.isDwl) {
isPlaceholder = modelData?.tag === -1; isPlaceholder = modelData?.tag === -1;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
isPlaceholder = modelData?.num === -1; isPlaceholder = modelData?.num === -1;
} else { } else {
isPlaceholder = modelData === -1; isPlaceholder = modelData === -1;
@@ -665,7 +679,7 @@ Item {
return getWorkspaceIndexFallback(modelData, index); return getWorkspaceIndexFallback(modelData, index);
} }
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
readonly property bool hasWorkspaces: getRealWorkspaces().length > 0 readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces) readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
@@ -865,7 +879,7 @@ Item {
return !!(modelData && modelData.id === root.currentWorkspace); return !!(modelData && modelData.id === root.currentWorkspace);
if (CompositorService.isDwl) if (CompositorService.isDwl)
return !!(modelData && root.dwlActiveTags.includes(modelData.tag)); return !!(modelData && root.dwlActiveTags.includes(modelData.tag));
if (CompositorService.isSway || CompositorService.isScroll) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return !!(modelData && modelData.num === root.currentWorkspace); return !!(modelData && modelData.num === root.currentWorkspace);
return modelData === root.currentWorkspace; return modelData === root.currentWorkspace;
} }
@@ -889,7 +903,7 @@ Item {
return !!(modelData && modelData.id === -1); return !!(modelData && modelData.id === -1);
if (CompositorService.isDwl) if (CompositorService.isDwl)
return !!(modelData && modelData.tag === -1); return !!(modelData && modelData.tag === -1);
if (CompositorService.isSway || CompositorService.isScroll) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return !!(modelData && modelData.num === -1); return !!(modelData && modelData.num === -1);
return modelData === -1; return modelData === -1;
} }
@@ -906,7 +920,7 @@ Item {
return loadedIsUrgent; return loadedIsUrgent;
if (CompositorService.isDwl) if (CompositorService.isDwl)
return modelData?.state === 2; return modelData?.state === 2;
if (CompositorService.isSway || CompositorService.isScroll) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return loadedIsUrgent; return loadedIsUrgent;
return false; return false;
} }
@@ -927,7 +941,7 @@ Item {
targetWorkspaceId = modelData?.id; targetWorkspaceId = modelData?.id;
} else if (CompositorService.isDwl) { } else if (CompositorService.isDwl) {
targetWorkspaceId = modelData?.tag; targetWorkspaceId = modelData?.tag;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
targetWorkspaceId = modelData?.num; targetWorkspaceId = modelData?.num;
} }
if (targetWorkspaceId === undefined || targetWorkspaceId === null) if (targetWorkspaceId === undefined || targetWorkspaceId === null)
@@ -946,7 +960,7 @@ Item {
let winWs = null; let winWs = null;
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
winWs = w.workspace_id; winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
winWs = w.workspace?.num; winWs = w.workspace?.num;
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []); const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
@@ -971,7 +985,7 @@ Item {
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5)
readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== "" readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== ""
readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && CompositorService.isNiri readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && (CompositorService.isNiri || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
readonly property real contentImplicitWidth: (hasWorkspaceName || loadedHasIcon) ? (appIconsLoader.item?.contentWidth ?? 0) : 0 readonly property real contentImplicitWidth: (hasWorkspaceName || loadedHasIcon) ? (appIconsLoader.item?.contentWidth ?? 0) : 0
readonly property real contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0 readonly property real contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0
@@ -1123,9 +1137,7 @@ Item {
return; return;
if (!dragHandler.dragging) { if (!dragHandler.dragging) {
const distance = root.isVertical const distance = root.isVertical ? Math.abs(mouse.y - dragHandler.dragStartPos.y) : Math.abs(mouse.x - dragHandler.dragStartPos.x);
? Math.abs(mouse.y - dragHandler.dragStartPos.y)
: Math.abs(mouse.x - dragHandler.dragStartPos.x);
if (distance > 5) { if (distance > 5) {
dragHandler.dragging = true; dragHandler.dragging = true;
root.dragSourceIndex = index; root.dragSourceIndex = index;
@@ -1136,9 +1148,7 @@ Item {
if (!dragHandler.dragging) if (!dragHandler.dragging)
return; return;
const rawAxisOffset = root.isVertical const rawAxisOffset = root.isVertical ? (mouse.y - dragHandler.dragStartPos.y) : (mouse.x - dragHandler.dragStartPos.x);
? (mouse.y - dragHandler.dragStartPos.y)
: (mouse.x - dragHandler.dragStartPos.x);
const itemSize = (root.isVertical ? delegateRoot.height : delegateRoot.width) + Theme.spacingS; const itemSize = (root.isVertical ? delegateRoot.height : delegateRoot.width) + Theme.spacingS;
const maxOffsetPositive = (root.workspaceList.length - 1 - index) * itemSize; const maxOffsetPositive = (root.workspaceList.length - 1 - index) * itemSize;
@@ -1189,7 +1199,7 @@ Item {
Hyprland.dispatch(`workspace ${modelData.id}`); Hyprland.dispatch(`workspace ${modelData.id}`);
} else if (CompositorService.isDwl && modelData?.tag !== undefined) { } else if (CompositorService.isDwl && modelData?.tag !== undefined) {
DwlService.switchToTag(root.screenName, modelData.tag); DwlService.switchToTag(root.screenName, modelData.tag);
} else if ((CompositorService.isSway || CompositorService.isScroll) && modelData?.num) { } else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num) {
try { try {
I3.dispatch(`workspace number ${modelData.num}`); I3.dispatch(`workspace number ${modelData.num}`);
} catch (_) {} } catch (_) {}
@@ -1228,7 +1238,7 @@ Item {
wsData = modelData; wsData = modelData;
} else if (CompositorService.isDwl) { } else if (CompositorService.isDwl) {
wsData = modelData; wsData = modelData;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
wsData = modelData; wsData = modelData;
} }
delegateRoot.loadedWorkspaceData = wsData; delegateRoot.loadedWorkspaceData = wsData;
@@ -1247,7 +1257,7 @@ Item {
delegateRoot.loadedHasIcon = icData !== null; delegateRoot.loadedHasIcon = icData !== null;
if (SettingsData.showWorkspaceApps) { if (SettingsData.showWorkspaceApps) {
if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll) { if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData); delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
} else if (CompositorService.isNiri) { } else if (CompositorService.isNiri) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData); delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
@@ -1760,7 +1770,7 @@ Item {
} }
Connections { Connections {
target: I3.workspaces target: I3.workspaces
enabled: (CompositorService.isSway || CompositorService.isScroll) enabled: (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
function onValuesChanged() { function onValuesChanged() {
delegateRoot.updateAllData(); delegateRoot.updateAllData();
} }

View File

@@ -74,6 +74,8 @@ Card {
return "on Sway"; return "on Sway";
if (CompositorService.isScroll) if (CompositorService.isScroll)
return "on Scroll"; return "on Scroll";
if (CompositorService.isMiracle)
return "on Miracle WM";
return ""; return "";
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall

View File

@@ -236,7 +236,7 @@ Item {
} }
IconImage { IconImage {
visible: SettingsData.dockLauncherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isLabwc) visible: SettingsData.dockLauncherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
anchors.centerIn: parent anchors.centerIn: parent
width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
@@ -253,6 +253,8 @@ Item {
return "file://" + Theme.shellDir + "/assets/sway.svg"; return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isScroll) { } else if (CompositorService.isScroll) {
return "file://" + Theme.shellDir + "/assets/sway.svg"; return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isMiracle) {
return "file://" + Theme.shellDir + "/assets/miraclewm.svg";
} else if (CompositorService.isLabwc) { } else if (CompositorService.isLabwc) {
return "file://" + Theme.shellDir + "/assets/labwc.png"; return "file://" + Theme.shellDir + "/assets/labwc.png";
} }

View File

@@ -14,7 +14,7 @@ dms-greeter - DankMaterialShell greeter launcher
Usage: dms-greeter --command COMPOSITOR [OPTIONS] Usage: dms-greeter --command COMPOSITOR [OPTIONS]
Required: Required:
--command COMPOSITOR Compositor to use (niri, hyprland, sway, scroll, mango, or labwc) --command COMPOSITOR Compositor to use (niri, hyprland, sway, scroll, miracle, mango, or labwc)
Options: Options:
-C, --config PATH Custom compositor config file -C, --config PATH Custom compositor config file
@@ -244,6 +244,24 @@ SCROLL_EOF
exec scroll -c "$COMPOSITOR_CONFIG" exec scroll -c "$COMPOSITOR_CONFIG"
;; ;;
miracle|miracle-wm)
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << MIRACLE_EOF
exec "$QS_CMD; miraclemsg exit"
MIRACLE_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << MIRACLE_EOF
exec "$QS_CMD; miraclemsg exit"
MIRACLE_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec miracle-wm -c "$COMPOSITOR_CONFIG"
;;
labwc) labwc)
if [[ -n "$COMPOSITOR_CONFIG" ]]; then if [[ -n "$COMPOSITOR_CONFIG" ]]; then
@@ -263,7 +281,7 @@ SCROLL_EOF
*) *)
echo "Error: Unsupported compositor: $COMPOSITOR" >&2 echo "Error: Unsupported compositor: $COMPOSITOR" >&2
echo "Supported compositors: niri, hyprland, sway, scroll, mango, labwc" >&2 echo "Supported compositors: niri, hyprland, sway, scroll, miracle, mango, labwc" >&2
exit 1 exit 1
;; ;;
esac esac

View File

@@ -14,6 +14,7 @@ Item {
property bool isNiri: CompositorService.isNiri property bool isNiri: CompositorService.isNiri
property bool isSway: CompositorService.isSway property bool isSway: CompositorService.isSway
property bool isScroll: CompositorService.isScroll property bool isScroll: CompositorService.isScroll
property bool isMiracle: CompositorService.isMiracle
property bool isDwl: CompositorService.isDwl property bool isDwl: CompositorService.isDwl
property bool isLabwc: CompositorService.isLabwc property bool isLabwc: CompositorService.isLabwc
@@ -24,6 +25,8 @@ Item {
return "sway"; return "sway";
if (isScroll) if (isScroll)
return "scroll"; return "scroll";
if (isMiracle)
return "miracle";
if (isDwl) if (isDwl)
return "mangowc"; return "mangowc";
if (isLabwc) if (isLabwc)
@@ -38,6 +41,8 @@ Item {
return "/assets/sway.svg"; return "/assets/sway.svg";
if (isScroll) if (isScroll)
return "/assets/sway.svg"; return "/assets/sway.svg";
if (isMiracle)
return "/assets/miraclewm.svg";
if (isDwl) if (isDwl)
return "/assets/mango.png"; return "/assets/mango.png";
if (isLabwc) if (isLabwc)
@@ -52,6 +57,8 @@ Item {
return "https://swaywm.org"; return "https://swaywm.org";
if (isScroll) if (isScroll)
return "https://github.com/dawsers/scroll"; return "https://github.com/dawsers/scroll";
if (isMiracle)
return "https://github.com/miracle-wm-org/miracle-wm";
if (isDwl) if (isDwl)
return "https://github.com/DreamMaoMao/mangowc"; return "https://github.com/DreamMaoMao/mangowc";
if (isLabwc) if (isLabwc)
@@ -66,6 +73,8 @@ Item {
return "Sway Website"; return "Sway Website";
if (isScroll) if (isScroll)
return "Scroll Github"; return "Scroll Github";
if (isMiracle)
return "Miracle WM GitHub";
if (isDwl) if (isDwl)
return "mangowc GitHub"; return "mangowc GitHub";
if (isLabwc) if (isLabwc)
@@ -98,9 +107,9 @@ Item {
property string ircUrl: "https://web.libera.chat/gamja/?channels=#labwc" property string ircUrl: "https://web.libera.chat/gamja/?channels=#labwc"
property string ircTooltip: "LabWC IRC Channel" property string ircTooltip: "LabWC IRC Channel"
property bool showMatrix: isNiri && !isHyprland && !isSway && !isScroll && !isDwl && !isLabwc property bool showMatrix: isNiri && !isHyprland && !isSway && !isScroll && !isMiracle && !isDwl && !isLabwc
property bool showCompositorDiscord: isHyprland || isDwl property bool showCompositorDiscord: isHyprland || isDwl
property bool showReddit: isNiri && !isHyprland && !isSway && !isScroll && !isDwl && !isLabwc property bool showReddit: isNiri && !isHyprland && !isSway && !isScroll && !isMiracle && !isDwl && !isLabwc
property bool showIrc: isLabwc property bool showIrc: isLabwc
DankFlickable { DankFlickable {

View File

@@ -264,6 +264,8 @@ Item {
modes.push("Sway"); modes.push("Sway");
} else if (CompositorService.isScroll) { } else if (CompositorService.isScroll) {
modes.push("Scroll"); modes.push("Scroll");
} else if (CompositorService.isMiracle) {
modes.push("Miracle");
} else { } else {
modes.push(I18n.tr("Compositor")); modes.push(I18n.tr("Compositor"));
} }

View File

@@ -67,6 +67,8 @@ Item {
modes.push("Sway"); modes.push("Sway");
} else if (CompositorService.isScroll) { } else if (CompositorService.isScroll) {
modes.push("Scroll"); modes.push("Scroll");
} else if (CompositorService.isMiracle) {
modes.push("Miracle");
} else { } else {
modes.push(I18n.tr("Compositor")); modes.push(I18n.tr("Compositor"));
} }

View File

@@ -131,7 +131,7 @@ Item {
text: I18n.tr("Follow Monitor Focus") text: I18n.tr("Follow Monitor Focus")
description: I18n.tr("Show workspaces of the currently focused monitor") description: I18n.tr("Show workspaces of the currently focused monitor")
checked: SettingsData.workspaceFollowFocus checked: SettingsData.workspaceFollowFocus
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
onToggled: checked => SettingsData.set("workspaceFollowFocus", checked) onToggled: checked => SettingsData.set("workspaceFollowFocus", checked)
} }
@@ -296,12 +296,12 @@ Item {
height: 1 height: 1
color: Theme.outline color: Theme.outline
opacity: 0.15 opacity: 0.15
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
} }
SettingsButtonGroupRow { SettingsButtonGroupRow {
text: I18n.tr("Urgent Color") text: I18n.tr("Urgent Color")
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
model: ["err", "pri", "sec", "s", "sc"] model: ["err", "pri", "sec", "s", "sc"]
buttonHeight: 22 buttonHeight: 22
minButtonWidth: 36 minButtonWidth: 36

View File

@@ -66,7 +66,7 @@ Singleton {
return Hyprland.focusedWorkspace.monitor.name; return Hyprland.focusedWorkspace.monitor.name;
if (CompositorService.isNiri && NiriService.currentOutput) if (CompositorService.isNiri && NiriService.currentOutput)
return NiriService.currentOutput; return NiriService.currentOutput;
if (CompositorService.isSway || CompositorService.isScroll) { if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || ""; return focusedWs?.monitor?.name || "";
} }

View File

@@ -16,6 +16,7 @@ Singleton {
property bool isDwl: false property bool isDwl: false
property bool isSway: false property bool isSway: false
property bool isScroll: false property bool isScroll: false
property bool isMiracle: false
property bool isLabwc: false property bool isLabwc: false
property string compositor: "unknown" property string compositor: "unknown"
readonly property bool useHyprlandFocusGrab: isHyprland && Quickshell.env("DMS_HYPRLAND_EXCLUSIVE_FOCUS") !== "1" readonly property bool useHyprlandFocusGrab: isHyprland && Quickshell.env("DMS_HYPRLAND_EXCLUSIVE_FOCUS") !== "1"
@@ -24,6 +25,7 @@ Singleton {
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET") readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
readonly property string swaySocket: Quickshell.env("SWAYSOCK") readonly property string swaySocket: Quickshell.env("SWAYSOCK")
readonly property string scrollSocket: Quickshell.env("SWAYSOCK") readonly property string scrollSocket: Quickshell.env("SWAYSOCK")
readonly property string miracleSocket: Quickshell.env("MIRACLESOCK")
readonly property string labwcPid: Quickshell.env("LABWC_PID") readonly property string labwcPid: Quickshell.env("LABWC_PID")
property bool useNiriSorting: isNiri && NiriService property bool useNiriSorting: isNiri && NiriService
@@ -74,7 +76,7 @@ Singleton {
screenName = Hyprland.focusedWorkspace.monitor.name; screenName = Hyprland.focusedWorkspace.monitor.name;
else if (isNiri && NiriService.currentOutput) else if (isNiri && NiriService.currentOutput)
screenName = NiriService.currentOutput; screenName = NiriService.currentOutput;
else if (isSway || isScroll) { else if (isSway || isScroll || isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
screenName = focusedWs?.monitor?.name || ""; screenName = focusedWs?.monitor?.name || "";
} else if (isDwl && DwlService.activeOutput) } else if (isDwl && DwlService.activeOutput)
@@ -443,12 +445,13 @@ Singleton {
} }
function detectCompositor() { function detectCompositor() {
if (hyprlandSignature && hyprlandSignature.length > 0 && !niriSocket && !swaySocket && !scrollSocket && !labwcPid) { if (hyprlandSignature && hyprlandSignature.length > 0 && !niriSocket && !swaySocket && !scrollSocket && !miracleSocket && !labwcPid) {
isHyprland = true; isHyprland = true;
isNiri = false; isNiri = false;
isDwl = false; isDwl = false;
isSway = false; isSway = false;
isScroll = false; isScroll = false;
isMiracle = false;
isLabwc = false; isLabwc = false;
compositor = "hyprland"; compositor = "hyprland";
console.info("CompositorService: Detected Hyprland"); console.info("CompositorService: Detected Hyprland");
@@ -463,6 +466,7 @@ Singleton {
isDwl = false; isDwl = false;
isSway = false; isSway = false;
isScroll = false; isScroll = false;
isMiracle = false;
isLabwc = false; isLabwc = false;
compositor = "niri"; compositor = "niri";
console.info("CompositorService: Detected Niri with socket:", niriSocket); console.info("CompositorService: Detected Niri with socket:", niriSocket);
@@ -472,7 +476,7 @@ Singleton {
return; return;
} }
if (swaySocket && swaySocket.length > 0 && !scrollSocket && scrollSocket.length == 0) { if (swaySocket && swaySocket.length > 0 && !scrollSocket && scrollSocket.length == 0 && !miracleSocket) {
Proc.runCommand("swaySocketCheck", ["test", "-S", swaySocket], (output, exitCode) => { Proc.runCommand("swaySocketCheck", ["test", "-S", swaySocket], (output, exitCode) => {
if (exitCode === 0) { if (exitCode === 0) {
isNiri = false; isNiri = false;
@@ -480,6 +484,7 @@ Singleton {
isDwl = false; isDwl = false;
isSway = true; isSway = true;
isScroll = false; isScroll = false;
isMiracle = false;
isLabwc = false; isLabwc = false;
compositor = "sway"; compositor = "sway";
console.info("CompositorService: Detected Sway with socket:", swaySocket); console.info("CompositorService: Detected Sway with socket:", swaySocket);
@@ -488,7 +493,24 @@ Singleton {
return; return;
} }
if (scrollSocket && scrollSocket.length > 0) { if (miracleSocket && miracleSocket.length > 0) {
Proc.runCommand("miracleSocketCheck", ["test", "-S", miracleSocket], (output, exitCode) => {
if (exitCode === 0) {
isNiri = false;
isHyprland = false;
isDwl = false;
isSway = false;
isScroll = false;
isMiracle = true;
isLabwc = false;
compositor = "miracle";
console.info("CompositorService: Detected Miracle WM with socket:", miracleSocket);
}
}, 0);
return;
}
if (scrollSocket && scrollSocket.length > 0 && !miracleSocket) {
Proc.runCommand("scrollSocketCheck", ["test", "-S", scrollSocket], (output, exitCode) => { Proc.runCommand("scrollSocketCheck", ["test", "-S", scrollSocket], (output, exitCode) => {
if (exitCode === 0) { if (exitCode === 0) {
isNiri = false; isNiri = false;
@@ -496,6 +518,7 @@ Singleton {
isDwl = false; isDwl = false;
isSway = false; isSway = false;
isScroll = true; isScroll = true;
isMiracle = false;
isLabwc = false; isLabwc = false;
compositor = "scroll"; compositor = "scroll";
console.info("CompositorService: Detected Scroll with socket:", scrollSocket); console.info("CompositorService: Detected Scroll with socket:", scrollSocket);
@@ -510,6 +533,7 @@ Singleton {
isDwl = false; isDwl = false;
isSway = false; isSway = false;
isScroll = false; isScroll = false;
isMiracle = false;
isLabwc = true; isLabwc = true;
compositor = "labwc"; compositor = "labwc";
console.info("CompositorService: Detected LabWC with PID:", labwcPid); console.info("CompositorService: Detected LabWC with PID:", labwcPid);
@@ -524,6 +548,7 @@ Singleton {
isDwl = false; isDwl = false;
isSway = false; isSway = false;
isScroll = false; isScroll = false;
isMiracle = false;
isLabwc = false; isLabwc = false;
compositor = "unknown"; compositor = "unknown";
console.warn("CompositorService: No compositor detected"); console.warn("CompositorService: No compositor detected");
@@ -546,6 +571,7 @@ Singleton {
isDwl = true; isDwl = true;
isSway = false; isSway = false;
isScroll = false; isScroll = false;
isMiracle = false;
isLabwc = false; isLabwc = false;
compositor = "dwl"; compositor = "dwl";
console.info("CompositorService: Detected DWL via DMS capability"); console.info("CompositorService: Detected DWL via DMS capability");
@@ -559,7 +585,7 @@ Singleton {
return Hyprland.dispatch("dpms off"); return Hyprland.dispatch("dpms off");
if (isDwl) if (isDwl)
return _dwlPowerOffMonitors(); return _dwlPowerOffMonitors();
if (isSway || isScroll) { if (isSway || isScroll || isMiracle) {
try { try {
I3.dispatch("output * dpms off"); I3.dispatch("output * dpms off");
} catch (_) {} } catch (_) {}
@@ -578,7 +604,7 @@ Singleton {
return Hyprland.dispatch("dpms on"); return Hyprland.dispatch("dpms on");
if (isDwl) if (isDwl)
return _dwlPowerOnMonitors(); return _dwlPowerOnMonitors();
if (isSway || isScroll) { if (isSway || isScroll || isMiracle) {
try { try {
I3.dispatch("output * dpms on"); I3.dispatch("output * dpms on");
} catch (_) {} } catch (_) {}

View File

@@ -11,83 +11,83 @@ Singleton {
property var groups: [] property var groups: []
property var _cachedWorkspaces: ({}) property var _cachedWorkspaces: ({})
signal stateChanged() signal stateChanged
Connections { Connections {
target: DMSService target: DMSService
function onCapabilitiesReceived() { function onCapabilitiesReceived() {
checkCapabilities() checkCapabilities();
} }
function onConnectionStateChanged() { function onConnectionStateChanged() {
if (DMSService.isConnected) { if (DMSService.isConnected) {
checkCapabilities() checkCapabilities();
} else { } else {
extWorkspaceAvailable = false extWorkspaceAvailable = false;
} }
} }
function onExtWorkspaceStateUpdate(data) { function onExtWorkspaceStateUpdate(data) {
if (extWorkspaceAvailable) { if (extWorkspaceAvailable) {
handleStateUpdate(data) handleStateUpdate(data);
} }
} }
} }
Component.onCompleted: { Component.onCompleted: {
if (DMSService.dmsAvailable) { if (DMSService.dmsAvailable) {
checkCapabilities() checkCapabilities();
} }
} }
function checkCapabilities() { function checkCapabilities() {
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) { if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
extWorkspaceAvailable = false extWorkspaceAvailable = false;
return return;
} }
const hasExtWorkspace = DMSService.capabilities.includes("extworkspace") const hasExtWorkspace = DMSService.capabilities.includes("extworkspace");
if (hasExtWorkspace && !extWorkspaceAvailable) { if (hasExtWorkspace && !extWorkspaceAvailable) {
if (typeof CompositorService !== "undefined") { if (typeof CompositorService !== "undefined") {
const useExtWorkspace = DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll) const useExtWorkspace = DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && !CompositorService.isMiracle);
if (!useExtWorkspace) { if (!useExtWorkspace) {
console.info("ExtWorkspaceService: ext-workspace available but compositor has native support") console.info("ExtWorkspaceService: ext-workspace available but compositor has native support");
extWorkspaceAvailable = false extWorkspaceAvailable = false;
return return;
} }
} }
extWorkspaceAvailable = true extWorkspaceAvailable = true;
console.info("ExtWorkspaceService: ext-workspace capability detected") console.info("ExtWorkspaceService: ext-workspace capability detected");
DMSService.addSubscription("extworkspace") DMSService.addSubscription("extworkspace");
requestState() requestState();
} else if (!hasExtWorkspace) { } else if (!hasExtWorkspace) {
extWorkspaceAvailable = false extWorkspaceAvailable = false;
} }
} }
function requestState() { function requestState() {
if (!DMSService.isConnected || !extWorkspaceAvailable) { if (!DMSService.isConnected || !extWorkspaceAvailable) {
return return;
} }
DMSService.sendRequest("extworkspace.getState", null, response => { DMSService.sendRequest("extworkspace.getState", null, response => {
if (response.result) { if (response.result) {
handleStateUpdate(response.result) handleStateUpdate(response.result);
} }
}) });
} }
function handleStateUpdate(state) { function handleStateUpdate(state) {
groups = state.groups || [] groups = state.groups || [];
if (groups.length === 0) { if (groups.length === 0) {
console.warn("ExtWorkspaceService: Received empty workspace groups from backend") console.warn("ExtWorkspaceService: Received empty workspace groups from backend");
} else { } else {
console.log("ExtWorkspaceService: Updated with", groups.length, "workspace groups") console.log("ExtWorkspaceService: Updated with", groups.length, "workspace groups");
} }
stateChanged() stateChanged();
} }
function activateWorkspace(workspaceID, groupID = "") { function activateWorkspace(workspaceID, groupID = "") {
if (!DMSService.isConnected || !extWorkspaceAvailable) { if (!DMSService.isConnected || !extWorkspaceAvailable) {
return return;
} }
DMSService.sendRequest("extworkspace.activateWorkspace", { DMSService.sendRequest("extworkspace.activateWorkspace", {
@@ -95,14 +95,14 @@ Singleton {
"groupID": groupID "groupID": groupID
}, response => { }, response => {
if (response.error) { if (response.error) {
console.warn("ExtWorkspaceService: activateWorkspace error:", response.error) console.warn("ExtWorkspaceService: activateWorkspace error:", response.error);
} }
}) });
} }
function deactivateWorkspace(workspaceID, groupID = "") { function deactivateWorkspace(workspaceID, groupID = "") {
if (!DMSService.isConnected || !extWorkspaceAvailable) { if (!DMSService.isConnected || !extWorkspaceAvailable) {
return return;
} }
DMSService.sendRequest("extworkspace.deactivateWorkspace", { DMSService.sendRequest("extworkspace.deactivateWorkspace", {
@@ -110,14 +110,14 @@ Singleton {
"groupID": groupID "groupID": groupID
}, response => { }, response => {
if (response.error) { if (response.error) {
console.warn("ExtWorkspaceService: deactivateWorkspace error:", response.error) console.warn("ExtWorkspaceService: deactivateWorkspace error:", response.error);
} }
}) });
} }
function removeWorkspace(workspaceID, groupID = "") { function removeWorkspace(workspaceID, groupID = "") {
if (!DMSService.isConnected || !extWorkspaceAvailable) { if (!DMSService.isConnected || !extWorkspaceAvailable) {
return return;
} }
DMSService.sendRequest("extworkspace.removeWorkspace", { DMSService.sendRequest("extworkspace.removeWorkspace", {
@@ -125,14 +125,14 @@ Singleton {
"groupID": groupID "groupID": groupID
}, response => { }, response => {
if (response.error) { if (response.error) {
console.warn("ExtWorkspaceService: removeWorkspace error:", response.error) console.warn("ExtWorkspaceService: removeWorkspace error:", response.error);
} }
}) });
} }
function createWorkspace(groupID, name) { function createWorkspace(groupID, name) {
if (!DMSService.isConnected || !extWorkspaceAvailable) { if (!DMSService.isConnected || !extWorkspaceAvailable) {
return return;
} }
DMSService.sendRequest("extworkspace.createWorkspace", { DMSService.sendRequest("extworkspace.createWorkspace", {
@@ -140,134 +140,138 @@ Singleton {
"name": name "name": name
}, response => { }, response => {
if (response.error) { if (response.error) {
console.warn("ExtWorkspaceService: createWorkspace error:", response.error) console.warn("ExtWorkspaceService: createWorkspace error:", response.error);
} }
}) });
} }
function getGroupForOutput(outputName) { function getGroupForOutput(outputName) {
for (const group of groups) { for (const group of groups) {
if (group.outputs && group.outputs.includes(outputName)) { if (group.outputs && group.outputs.includes(outputName)) {
return group return group;
} }
} }
return null return null;
} }
function getWorkspacesForOutput(outputName) { function getWorkspacesForOutput(outputName) {
const group = getGroupForOutput(outputName) const group = getGroupForOutput(outputName);
return group ? (group.workspaces || []) : [] return group ? (group.workspaces || []) : [];
} }
function getActiveWorkspaces() { function getActiveWorkspaces() {
const active = [] const active = [];
for (const group of groups) { for (const group of groups) {
if (!group.workspaces) continue if (!group.workspaces)
continue;
for (const ws of group.workspaces) { for (const ws of group.workspaces) {
if (ws.active) { if (ws.active) {
active.push({ active.push({
workspace: ws, workspace: ws,
group: group, group: group,
outputs: group.outputs || [] outputs: group.outputs || []
}) });
} }
} }
} }
return active return active;
} }
function getActiveWorkspaceForOutput(outputName) { function getActiveWorkspaceForOutput(outputName) {
const group = getGroupForOutput(outputName) const group = getGroupForOutput(outputName);
if (!group || !group.workspaces) return null if (!group || !group.workspaces)
return null;
for (const ws of group.workspaces) { for (const ws of group.workspaces) {
if (ws.active) { if (ws.active) {
return ws return ws;
} }
} }
return null return null;
} }
function getVisibleWorkspaces(outputName) { function getVisibleWorkspaces(outputName) {
const workspaces = getWorkspacesForOutput(outputName) const workspaces = getWorkspacesForOutput(outputName);
let visible = workspaces.filter(ws => !ws.hidden) let visible = workspaces.filter(ws => !ws.hidden);
const hasValidCoordinates = visible.some(ws => ws.coordinates && ws.coordinates.length > 0) const hasValidCoordinates = visible.some(ws => ws.coordinates && ws.coordinates.length > 0);
if (hasValidCoordinates) { if (hasValidCoordinates) {
visible = visible.sort((a, b) => { visible = visible.sort((a, b) => {
const coordsA = a.coordinates || [0, 0] const coordsA = a.coordinates || [0, 0];
const coordsB = b.coordinates || [0, 0] const coordsB = b.coordinates || [0, 0];
if (coordsA[0] !== coordsB[0]) return coordsA[0] - coordsB[0] if (coordsA[0] !== coordsB[0])
return coordsA[1] - coordsB[1] return coordsA[0] - coordsB[0];
}) return coordsA[1] - coordsB[1];
});
} }
const cacheKey = outputName const cacheKey = outputName;
if (!_cachedWorkspaces[cacheKey]) { if (!_cachedWorkspaces[cacheKey]) {
_cachedWorkspaces[cacheKey] = { _cachedWorkspaces[cacheKey] = {
workspaces: [], workspaces: [],
lastNames: [] lastNames: []
} };
} }
const cache = _cachedWorkspaces[cacheKey] const cache = _cachedWorkspaces[cacheKey];
const currentNames = visible.map(ws => ws.name || ws.id) const currentNames = visible.map(ws => ws.name || ws.id);
const namesChanged = JSON.stringify(cache.lastNames) !== JSON.stringify(currentNames) const namesChanged = JSON.stringify(cache.lastNames) !== JSON.stringify(currentNames);
if (namesChanged || cache.workspaces.length !== visible.length) { if (namesChanged || cache.workspaces.length !== visible.length) {
cache.workspaces = visible.map(ws => ({ cache.workspaces = visible.map(ws => ({
id: ws.id, id: ws.id,
name: ws.name, name: ws.name,
coordinates: ws.coordinates, coordinates: ws.coordinates,
state: ws.state, state: ws.state,
active: ws.active, active: ws.active,
urgent: ws.urgent, urgent: ws.urgent,
hidden: ws.hidden hidden: ws.hidden
})) }));
cache.lastNames = currentNames cache.lastNames = currentNames;
return cache.workspaces return cache.workspaces;
} }
for (let i = 0; i < visible.length; i++) { for (let i = 0; i < visible.length; i++) {
const src = visible[i] const src = visible[i];
const dst = cache.workspaces[i] const dst = cache.workspaces[i];
dst.id = src.id dst.id = src.id;
dst.name = src.name dst.name = src.name;
dst.coordinates = src.coordinates dst.coordinates = src.coordinates;
dst.state = src.state dst.state = src.state;
dst.active = src.active dst.active = src.active;
dst.urgent = src.urgent dst.urgent = src.urgent;
dst.hidden = src.hidden dst.hidden = src.hidden;
} }
return cache.workspaces return cache.workspaces;
} }
function getUrgentWorkspaces() { function getUrgentWorkspaces() {
const urgent = [] const urgent = [];
for (const group of groups) { for (const group of groups) {
if (!group.workspaces) continue if (!group.workspaces)
continue;
for (const ws of group.workspaces) { for (const ws of group.workspaces) {
if (ws.urgent) { if (ws.urgent) {
urgent.push({ urgent.push({
workspace: ws, workspace: ws,
group: group, group: group,
outputs: group.outputs || [] outputs: group.outputs || []
}) });
} }
} }
} }
return urgent return urgent;
} }
function switchToWorkspace(outputName, workspaceName) { function switchToWorkspace(outputName, workspaceName) {
const workspaces = getWorkspacesForOutput(outputName) const workspaces = getWorkspacesForOutput(outputName);
for (const ws of workspaces) { for (const ws of workspaces) {
if (ws.name === workspaceName || ws.id === workspaceName) { if (ws.name === workspaceName || ws.id === workspaceName) {
activateWorkspace(ws.name || ws.id) activateWorkspace(ws.name || ws.id);
return return;
} }
} }
console.warn("ExtWorkspaceService: workspace not found:", workspaceName) console.warn("ExtWorkspaceService: workspace not found:", workspaceName);
} }
} }

View File

@@ -314,7 +314,7 @@ Singleton {
return; return;
} }
if (CompositorService.isSway || CompositorService.isScroll) { if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
try { try {
I3.dispatch("exit"); I3.dispatch("exit");
} catch (_) {} } catch (_) {}

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 131 KiB