diff --git a/README.md b/README.md index fe8a07c5..17c1bb9e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/) -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) diff --git a/core/cmd/dms/commands_keybinds.go b/core/cmd/dms/commands_keybinds.go index 51096187..b2de203e 100644 --- a/core/cmd/dms/commands_keybinds.go +++ b/core/cmd/dms/commands_keybinds.go @@ -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: diff --git a/core/go.mod b/core/go.mod index 919bc1ed..895b0347 100644 --- a/core/go.mod +++ b/core/go.mod @@ -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. diff --git a/core/go.sum b/core/go.sum index 7ed0bd1a..ebc72da2 100644 --- a/core/go.sum +++ b/core/go.sum @@ -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= diff --git a/core/internal/keybinds/providers/miracle.go b/core/internal/keybinds/providers/miracle.go new file mode 100644 index 00000000..f42d7e02 --- /dev/null +++ b/core/internal/keybinds/providers/miracle.go @@ -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" + } +} diff --git a/core/internal/keybinds/providers/miracle_parser.go b/core/internal/keybinds/providers/miracle_parser.go new file mode 100644 index 00000000..425c64ac --- /dev/null +++ b/core/internal/keybinds/providers/miracle_parser.go @@ -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 +} diff --git a/core/internal/keybinds/providers/sway.go b/core/internal/keybinds/providers/sway.go index 26c341f4..9ed41e33 100644 --- a/core/internal/keybinds/providers/sway.go +++ b/core/internal/keybinds/providers/sway.go @@ -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" } diff --git a/core/internal/screenshot/compositor.go b/core/internal/screenshot/compositor.go index 79e7d007..bbbf7cfe 100644 --- a/core/internal/screenshot/compositor.go +++ b/core/internal/screenshot/compositor.go @@ -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: diff --git a/distro/nix/greeter.nix b/distro/nix/greeter.nix index 13816220..d8ea5aea 100644 --- a/distro/nix/greeter.nix +++ b/distro/nix/greeter.nix @@ -73,6 +73,7 @@ in "labwc" "mango" "scroll" + "miracle" ]; description = "Compositor to run greeter in"; }; diff --git a/quickshell/DMSShellIPC.qml b/quickshell/DMSShellIPC.qml index 39879516..e86e11b4 100644 --- a/quickshell/DMSShellIPC.qml +++ b/quickshell/DMSShellIPC.qml @@ -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 || ""; } diff --git a/quickshell/Modules/DankBar/DankBar.qml b/quickshell/Modules/DankBar/DankBar.qml index 8b97fdd6..b3534ec3 100644 --- a/quickshell/Modules/DankBar/DankBar.qml +++ b/quickshell/Modules/DankBar/DankBar.qml @@ -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) { diff --git a/quickshell/Modules/DankBar/DankBarContent.qml b/quickshell/Modules/DankBar/DankBarContent.qml index b8d327d4..6ff34979 100644 --- a/quickshell/Modules/DankBar/DankBarContent.qml +++ b/quickshell/Modules/DankBar/DankBarContent.qml @@ -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; diff --git a/quickshell/Modules/DankBar/Widgets/LauncherButton.qml b/quickshell/Modules/DankBar/Widgets/LauncherButton.qml index 7840d1f7..73f8dac8 100644 --- a/quickshell/Modules/DankBar/Widgets/LauncherButton.qml +++ b/quickshell/Modules/DankBar/Widgets/LauncherButton.qml @@ -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"; } diff --git a/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml b/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml index f6ee3f05..8f26c77a 100644 --- a/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml +++ b/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml @@ -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(); } diff --git a/quickshell/Modules/DankDash/Overview/UserInfoCard.qml b/quickshell/Modules/DankDash/Overview/UserInfoCard.qml index af0bfd7c..666acbb3 100644 --- a/quickshell/Modules/DankDash/Overview/UserInfoCard.qml +++ b/quickshell/Modules/DankDash/Overview/UserInfoCard.qml @@ -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 diff --git a/quickshell/Modules/Dock/DockLauncherButton.qml b/quickshell/Modules/Dock/DockLauncherButton.qml index 10592e9b..84b52bab 100644 --- a/quickshell/Modules/Dock/DockLauncherButton.qml +++ b/quickshell/Modules/Dock/DockLauncherButton.qml @@ -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"; } diff --git a/quickshell/Modules/Greetd/assets/dms-greeter b/quickshell/Modules/Greetd/assets/dms-greeter index 72508eb3..7a5cfdcd 100755 --- a/quickshell/Modules/Greetd/assets/dms-greeter +++ b/quickshell/Modules/Greetd/assets/dms-greeter @@ -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 diff --git a/quickshell/Modules/Settings/AboutTab.qml b/quickshell/Modules/Settings/AboutTab.qml index 42fb8daa..d2d9b14a 100644 --- a/quickshell/Modules/Settings/AboutTab.qml +++ b/quickshell/Modules/Settings/AboutTab.qml @@ -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 { diff --git a/quickshell/Modules/Settings/DockTab.qml b/quickshell/Modules/Settings/DockTab.qml index 9a070534..7f7a2f1d 100644 --- a/quickshell/Modules/Settings/DockTab.qml +++ b/quickshell/Modules/Settings/DockTab.qml @@ -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")); } diff --git a/quickshell/Modules/Settings/LauncherTab.qml b/quickshell/Modules/Settings/LauncherTab.qml index 7df07911..e6bd6546 100644 --- a/quickshell/Modules/Settings/LauncherTab.qml +++ b/quickshell/Modules/Settings/LauncherTab.qml @@ -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")); } diff --git a/quickshell/Modules/Settings/WorkspacesTab.qml b/quickshell/Modules/Settings/WorkspacesTab.qml index 4d34b519..8c0e4da3 100644 --- a/quickshell/Modules/Settings/WorkspacesTab.qml +++ b/quickshell/Modules/Settings/WorkspacesTab.qml @@ -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 diff --git a/quickshell/Services/BarWidgetService.qml b/quickshell/Services/BarWidgetService.qml index 3a0c18fa..205a8fd8 100644 --- a/quickshell/Services/BarWidgetService.qml +++ b/quickshell/Services/BarWidgetService.qml @@ -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 || ""; } diff --git a/quickshell/Services/CompositorService.qml b/quickshell/Services/CompositorService.qml index cbdef182..5bfa7bda 100644 --- a/quickshell/Services/CompositorService.qml +++ b/quickshell/Services/CompositorService.qml @@ -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 (_) {} diff --git a/quickshell/Services/ExtWorkspaceService.qml b/quickshell/Services/ExtWorkspaceService.qml index e6784827..48a8f9a1 100644 --- a/quickshell/Services/ExtWorkspaceService.qml +++ b/quickshell/Services/ExtWorkspaceService.qml @@ -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,134 +140,138 @@ 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 => ({ - id: ws.id, - name: ws.name, - coordinates: ws.coordinates, - state: ws.state, - active: ws.active, - urgent: ws.urgent, - hidden: ws.hidden - })) - cache.lastNames = currentNames - return cache.workspaces + id: ws.id, + name: ws.name, + coordinates: ws.coordinates, + state: ws.state, + active: ws.active, + urgent: ws.urgent, + hidden: ws.hidden + })); + 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); } } diff --git a/quickshell/Services/SessionService.qml b/quickshell/Services/SessionService.qml index 27d7a645..50adaf88 100644 --- a/quickshell/Services/SessionService.qml +++ b/quickshell/Services/SessionService.qml @@ -314,7 +314,7 @@ Singleton { return; } - if (CompositorService.isSway || CompositorService.isScroll) { + if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { try { I3.dispatch("exit"); } catch (_) {} diff --git a/quickshell/assets/miraclewm.svg b/quickshell/assets/miraclewm.svg new file mode 100644 index 00000000..6fdd1e04 --- /dev/null +++ b/quickshell/assets/miraclewm.svg @@ -0,0 +1,1198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +