1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-16 02:32:09 -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>
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
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## 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)

View File

@@ -97,6 +97,11 @@ func initializeProviders() {
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")
if err := registry.Register(swayProvider); err != nil {
log.Warnf("Failed to register Sway provider: %v", err)
@@ -144,6 +149,8 @@ func makeProviderWithPath(name, path string) keybinds.Provider {
return providers.NewSwayProvider(path)
case "scroll":
return providers.NewSwayProvider(path)
case "miracle":
return providers.NewMiracleProvider(path)
case "niri":
return providers.NewNiriProvider(path)
default:

View File

@@ -19,7 +19,7 @@ require (
github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
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
)
@@ -27,7 +27,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.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/cyphar/filepath-securejoin v0.6.1 // 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-logfmt/logfmt v0.6.1 // 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/pjbgf/sha1cd v0.5.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/davecgh/go-spew v1.1.1 // 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/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -71,7 +71,7 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.41.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.

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/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
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.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
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/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
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-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/v6 v6.0.0-20260210102253-e4d10f0e569a h1:LLju0NuXQqR4WmGl1Dm86b9ZXsvXgLYbx/aaAjdQr6w=
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 h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s=
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/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
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.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
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/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
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-20260211191109-2735e65f0518/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
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/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
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"
}
} else {
// Determine isScroll based on the provided config path
isScroll = strings.Contains(configPath, "scroll")
}
@@ -36,16 +35,16 @@ func NewSwayProvider(configPath string) *SwayProvider {
}
func (s *SwayProvider) Name() string {
if s != nil && s.isScroll {
return "scroll"
}
if s == nil {
_, ok := os.LookupEnv("SCROLLSOCK")
if ok {
if os.Getenv("SCROLLSOCK") != "" {
return "scroll"
}
return "sway"
}
if s.isScroll {
return "scroll"
}
return "sway"
}

View File

@@ -21,6 +21,7 @@ const (
CompositorNiri
CompositorDWL
CompositorScroll
CompositorMiracle
)
var detectedCompositor Compositor = -1
@@ -34,6 +35,7 @@ func DetectCompositor() Compositor {
niriSocket := os.Getenv("NIRI_SOCKET")
swaySocket := os.Getenv("SWAYSOCK")
scrollSocket := os.Getenv("SCROLLSOCK")
miracleSocket := os.Getenv("MIRACLESOCK")
switch {
case niriSocket != "":
@@ -46,7 +48,11 @@ func DetectCompositor() Compositor {
detectedCompositor = CompositorScroll
return detectedCompositor
}
case miracleSocket != "":
if _, err := os.Stat(miracleSocket); err == nil {
detectedCompositor = CompositorMiracle
return detectedCompositor
}
case swaySocket != "":
if _, err := os.Stat(swaySocket); err == nil {
detectedCompositor = CompositorSway
@@ -260,6 +266,25 @@ func getScrollFocusedMonitor() string {
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 {
Output string `json:"output"`
IsFocused bool `json:"is_focused"`
@@ -407,6 +432,8 @@ func GetFocusedMonitor() string {
return getSwayFocusedMonitor()
case CompositorScroll:
return getScrollFocusedMonitor()
case CompositorMiracle:
return getMiracleFocusedMonitor()
case CompositorNiri:
return getNiriFocusedMonitor()
case CompositorDWL:

View File

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

View File

@@ -197,7 +197,7 @@ Item {
if (CompositorService.isNiri && 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);
return focusedWs?.monitor?.name || "";
}

View File

@@ -96,7 +96,7 @@ Item {
focusedScreenName = Hyprland.focusedWorkspace.monitor.name;
} else if (CompositorService.isNiri && 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);
focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {
@@ -125,7 +125,7 @@ Item {
focusedScreenName = Hyprland.focusedWorkspace.monitor.name;
} else if (CompositorService.isNiri && 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);
focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {

View File

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

View File

@@ -55,7 +55,7 @@ BasePill {
}
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
width: 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";
} else if (CompositorService.isScroll) {
return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isMiracle) {
return "file://" + Theme.shellDir + "/assets/miraclewm.svg";
} else if (CompositorService.isLabwc) {
return "file://" + Theme.shellDir + "/assets/labwc.png";
}

View File

@@ -37,6 +37,7 @@ Item {
return DwlService.activeOutput || root.screenName;
case "sway":
case "scroll":
case "miracle":
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || root.screenName;
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 {
target: DesktopEntries
@@ -67,6 +68,7 @@ Item {
return activeTags.length > 0 ? activeTags[0] : -1;
case "sway":
case "scroll":
case "miracle":
return getSwayActiveWorkspace();
default:
return 1;
@@ -97,6 +99,7 @@ Item {
break;
case "sway":
case "scroll":
case "miracle":
baseList = getSwayWorkspaces();
break;
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) {
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);
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
}
@@ -222,7 +236,7 @@ Item {
return [];
}
targetWorkspaceId = ws.tag;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
targetWorkspaceId = ws.num !== undefined ? ws.num : ws;
} else {
return [];
@@ -234,7 +248,7 @@ Item {
let isActiveWs = false;
if (CompositorService.isNiri) {
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);
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false;
} else if (CompositorService.isDwl) {
@@ -255,7 +269,7 @@ Item {
let winWs = null;
if (CompositorService.isNiri) {
winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
winWs = w.workspace?.num;
} else {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
@@ -322,7 +336,7 @@ Item {
placeholder = {
"tag": -1
};
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
placeholder = {
"num": -1
};
@@ -516,7 +530,7 @@ Item {
return ws && ws.id !== -1;
if (CompositorService.isDwl)
return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll)
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return ws && ws.num !== -1;
return ws !== -1;
});
@@ -588,7 +602,7 @@ Item {
}
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) {
return;
@@ -617,7 +631,7 @@ Item {
return modelData?.id || "";
if (CompositorService.isDwl)
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
if (CompositorService.isSway || CompositorService.isScroll)
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return modelData?.num || "";
return modelData - 1;
}
@@ -632,7 +646,7 @@ Item {
isPlaceholder = modelData?.id === -1;
} else if (CompositorService.isDwl) {
isPlaceholder = modelData?.tag === -1;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
isPlaceholder = modelData?.num === -1;
} else {
isPlaceholder = modelData === -1;
@@ -665,7 +679,7 @@ Item {
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 shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
@@ -865,7 +879,7 @@ Item {
return !!(modelData && modelData.id === root.currentWorkspace);
if (CompositorService.isDwl)
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 === root.currentWorkspace;
}
@@ -889,7 +903,7 @@ Item {
return !!(modelData && modelData.id === -1);
if (CompositorService.isDwl)
return !!(modelData && modelData.tag === -1);
if (CompositorService.isSway || CompositorService.isScroll)
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return !!(modelData && modelData.num === -1);
return modelData === -1;
}
@@ -906,7 +920,7 @@ Item {
return loadedIsUrgent;
if (CompositorService.isDwl)
return modelData?.state === 2;
if (CompositorService.isSway || CompositorService.isScroll)
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return loadedIsUrgent;
return false;
}
@@ -927,7 +941,7 @@ Item {
targetWorkspaceId = modelData?.id;
} else if (CompositorService.isDwl) {
targetWorkspaceId = modelData?.tag;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
targetWorkspaceId = modelData?.num;
}
if (targetWorkspaceId === undefined || targetWorkspaceId === null)
@@ -946,7 +960,7 @@ Item {
let winWs = null;
if (CompositorService.isNiri) {
winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
winWs = w.workspace?.num;
} else if (CompositorService.isHyprland) {
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 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 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 contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0
@@ -1123,9 +1137,7 @@ Item {
return;
if (!dragHandler.dragging) {
const distance = root.isVertical
? Math.abs(mouse.y - dragHandler.dragStartPos.y)
: Math.abs(mouse.x - dragHandler.dragStartPos.x);
const distance = root.isVertical ? Math.abs(mouse.y - dragHandler.dragStartPos.y) : Math.abs(mouse.x - dragHandler.dragStartPos.x);
if (distance > 5) {
dragHandler.dragging = true;
root.dragSourceIndex = index;
@@ -1136,9 +1148,7 @@ Item {
if (!dragHandler.dragging)
return;
const rawAxisOffset = root.isVertical
? (mouse.y - dragHandler.dragStartPos.y)
: (mouse.x - dragHandler.dragStartPos.x);
const rawAxisOffset = root.isVertical ? (mouse.y - dragHandler.dragStartPos.y) : (mouse.x - dragHandler.dragStartPos.x);
const itemSize = (root.isVertical ? delegateRoot.height : delegateRoot.width) + Theme.spacingS;
const maxOffsetPositive = (root.workspaceList.length - 1 - index) * itemSize;
@@ -1189,7 +1199,7 @@ Item {
Hyprland.dispatch(`workspace ${modelData.id}`);
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
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 {
I3.dispatch(`workspace number ${modelData.num}`);
} catch (_) {}
@@ -1228,7 +1238,7 @@ Item {
wsData = modelData;
} else if (CompositorService.isDwl) {
wsData = modelData;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
wsData = modelData;
}
delegateRoot.loadedWorkspaceData = wsData;
@@ -1247,7 +1257,7 @@ Item {
delegateRoot.loadedHasIcon = icData !== null;
if (SettingsData.showWorkspaceApps) {
if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll) {
if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
} else if (CompositorService.isNiri) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
@@ -1760,7 +1770,7 @@ Item {
}
Connections {
target: I3.workspaces
enabled: (CompositorService.isSway || CompositorService.isScroll)
enabled: (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
function onValuesChanged() {
delegateRoot.updateAllData();
}

View File

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

View File

@@ -236,7 +236,7 @@ Item {
}
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
width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
@@ -253,6 +253,8 @@ Item {
return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isScroll) {
return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isMiracle) {
return "file://" + Theme.shellDir + "/assets/miraclewm.svg";
} else if (CompositorService.isLabwc) {
return "file://" + Theme.shellDir + "/assets/labwc.png";
}

View File

@@ -14,7 +14,7 @@ dms-greeter - DankMaterialShell greeter launcher
Usage: dms-greeter --command COMPOSITOR [OPTIONS]
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:
-C, --config PATH Custom compositor config file
@@ -244,6 +244,24 @@ SCROLL_EOF
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)
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
@@ -263,7 +281,7 @@ SCROLL_EOF
*)
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
;;
esac

View File

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

View File

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

View File

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

View File

@@ -131,7 +131,7 @@ Item {
text: I18n.tr("Follow Monitor Focus")
description: I18n.tr("Show workspaces of the currently focused monitor")
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)
}
@@ -296,12 +296,12 @@ Item {
height: 1
color: Theme.outline
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 {
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"]
buttonHeight: 22
minButtonWidth: 36

View File

@@ -66,7 +66,7 @@ Singleton {
return Hyprland.focusedWorkspace.monitor.name;
if (CompositorService.isNiri && 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);
return focusedWs?.monitor?.name || "";
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 131 KiB