From c87fa0de5e19e780c28b3dfff9c20f4a4a13187a Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 29 Oct 2025 15:08:11 -0400 Subject: [PATCH] sway: add support for sway --- .github/ISSUE_TEMPLATE/bug_report.md | 1 + .github/ISSUE_TEMPLATE/feature_request.md | 1 + .github/ISSUE_TEMPLATE/support_request.md | 1 + DMSShell.qml | 2 +- Modules/DankBar/DankBar.qml | 34 ++++++++ Modules/DankBar/Widgets/LauncherButton.qml | 4 +- Modules/DankBar/Widgets/WorkspaceSwitcher.qml | 86 +++++++++++++++++-- Modules/Settings/LauncherTab.qml | 2 + README.md | 8 +- Services/CompositorService.qml | 46 ++++++---- Services/SessionService.qml | 6 ++ assets/sway.svg | 47 ++++++++++ 12 files changed, 212 insertions(+), 26 deletions(-) create mode 100644 assets/sway.svg diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8c27871a..c4c73553 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -25,6 +25,7 @@ assignees: "" - [ ] niri - [ ] Hyprland - [ ] dwl (MangoWC) +- [ ] sway - [ ] Other (specify) ## Distribution diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 33664034..ead756aa 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -22,6 +22,7 @@ Is this feature specific to one compositor? - [ ] niri - [ ] Hyprland - [ ] dwl (MangoWC) +- [ ] sway ## Proposed Solution diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md index 5b777b51..0aa9d396 100644 --- a/.github/ISSUE_TEMPLATE/support_request.md +++ b/.github/ISSUE_TEMPLATE/support_request.md @@ -11,6 +11,7 @@ assignees: "" - [ ] niri - [ ] Hyprland - [ ] dwl (MangoWC) +- [ ] sway - [ ] other ## Distribution diff --git a/DMSShell.qml b/DMSShell.qml index 686e9f61..be68a883 100644 --- a/DMSShell.qml +++ b/DMSShell.qml @@ -54,7 +54,7 @@ Item { Loader { id: blurredWallpaperBackgroundLoader - active: SettingsData.blurredWallpaperLayer + active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri asynchronous: false sourceComponent: BlurredWallpaperBackground {} diff --git a/Modules/DankBar/DankBar.qml b/Modules/DankBar/DankBar.qml index 2dfbf15c..1fd15b12 100644 --- a/Modules/DankBar/DankBar.qml +++ b/Modules/DankBar/DankBar.qml @@ -4,6 +4,7 @@ import QtQuick.Effects import QtQuick.Shapes import Quickshell import Quickshell.Hyprland +import Quickshell.I3 import Quickshell.Io import Quickshell.Services.Mpris import Quickshell.Services.Notifications @@ -31,6 +32,9 @@ Item { focusedScreenName = Hyprland.focusedWorkspace.monitor.name } else if (CompositorService.isNiri && NiriService.currentOutput) { focusedScreenName = NiriService.currentOutput + } else if (CompositorService.isSway) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true) + focusedScreenName = focusedWs?.monitor?.name || "" } if (!focusedScreenName && barVariants.instances.length > 0) { @@ -55,6 +59,9 @@ Item { focusedScreenName = Hyprland.focusedWorkspace.monitor.name } else if (CompositorService.isNiri && NiriService.currentOutput) { focusedScreenName = NiriService.currentOutput + } else if (CompositorService.isSway) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true) + focusedScreenName = focusedWs?.monitor?.name || "" } if (!focusedScreenName && barVariants.instances.length > 0) { @@ -573,6 +580,16 @@ Item { return Array.from({length: 9}, (_, i) => i) } return Array.from({length: DwlService.tagCount}, (_, i) => i) + } else if (CompositorService.isSway) { + const workspaces = I3.workspaces?.values || [] + if (workspaces.length === 0) return [{"num": 1}] + + if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) { + return workspaces.slice().sort((a, b) => a.num - b.num) + } + + const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === barWindow.screenName) + return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [{"num": 1}] } return [1] } @@ -594,6 +611,14 @@ Item { if (!outputState || !outputState.tags) return 0 const activeTags = DwlService.getActiveTags(barWindow.screenName) return activeTags.length > 0 ? activeTags[0] : 0 + } else if (CompositorService.isSway) { + if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true) + return focusedWs ? focusedWs.num : 1 + } + + const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === barWindow.screenName && ws.focused === true) + return focusedWs ? focusedWs.num : 1 } return 1 } @@ -631,6 +656,15 @@ Item { if (nextIndex !== validIndex) { DwlService.switchToTag(barWindow.screenName, realWorkspaces[nextIndex]) } + } else if (CompositorService.isSway) { + const currentWs = getCurrentWorkspace() + const currentIndex = realWorkspaces.findIndex(ws => ws.num === currentWs) + const validIndex = currentIndex === -1 ? 0 : currentIndex + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0) + + if (nextIndex !== validIndex) { + try { I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`) } catch(_){} + } } } diff --git a/Modules/DankBar/Widgets/LauncherButton.qml b/Modules/DankBar/Widgets/LauncherButton.qml index da27f424..2a9ee050 100644 --- a/Modules/DankBar/Widgets/LauncherButton.qml +++ b/Modules/DankBar/Widgets/LauncherButton.qml @@ -37,7 +37,7 @@ BasePill { } IconImage { - visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl) + visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway) anchors.centerIn: parent width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset) height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset) @@ -50,6 +50,8 @@ BasePill { return "file://" + Theme.shellDir + "/assets/hyprland.svg" } else if (CompositorService.isDwl) { return "file://" + Theme.shellDir + "/assets/mango.png" + } else if (CompositorService.isSway) { + return "file://" + Theme.shellDir + "/assets/sway.svg" } return "" } diff --git a/Modules/DankBar/Widgets/WorkspaceSwitcher.qml b/Modules/DankBar/Widgets/WorkspaceSwitcher.qml index a8c3a7eb..0f244bcf 100644 --- a/Modules/DankBar/Widgets/WorkspaceSwitcher.qml +++ b/Modules/DankBar/Widgets/WorkspaceSwitcher.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import Quickshell import Quickshell.Widgets import Quickshell.Hyprland +import Quickshell.I3 import qs.Common import qs.Services import qs.Widgets @@ -36,6 +37,8 @@ Item { return getHyprlandActiveWorkspace() } else if (CompositorService.isDwl) { return getDwlActiveTag() + } else if (CompositorService.isSway) { + return getSwayActiveWorkspace() } return 1 } @@ -46,7 +49,6 @@ Item { } if (CompositorService.isHyprland) { const baseList = getHyprlandWorkspaces() - // Filter out special workspaces const filteredList = baseList.filter(ws => ws.id > -1) return SettingsData.showWorkspacePadding ? padWorkspaces(filteredList) : filteredList } @@ -54,9 +56,35 @@ Item { const baseList = getDwlTags() return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList } + if (CompositorService.isSway) { + const baseList = getSwayWorkspaces() + return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList + } return [1] } + function getSwayWorkspaces() { + const workspaces = I3.workspaces?.values || [] + if (workspaces.length === 0) return [{"num": 1}] + + if (!root.screenName || !SettingsData.workspacesPerMonitor) { + return workspaces.slice().sort((a, b) => a.num - b.num) + } + + const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName) + return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [{"num": 1}] + } + + function getSwayActiveWorkspace() { + if (!root.screenName || !SettingsData.workspacesPerMonitor) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true) + return focusedWs ? focusedWs.num : 1 + } + + const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === root.screenName && ws.focused === true) + return focusedWs ? focusedWs.num : 1 + } + function getWorkspaceIcons(ws) { _desktopEntriesUpdateTrigger if (!SettingsData.showWorkspaceApps || !ws) { @@ -81,6 +109,8 @@ Item { return [] } targetWorkspaceId = ws.tag + } else if (CompositorService.isSway) { + targetWorkspaceId = ws.num !== undefined ? ws.num : ws } else { return [] } @@ -91,6 +121,9 @@ Item { let isActiveWs = false if (CompositorService.isNiri) { isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active) + } else if (CompositorService.isSway) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true) + isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false } else if (CompositorService.isDwl) { const output = DwlService.getOutputState(root.screenName) if (output && output.tags) { @@ -109,8 +142,9 @@ Item { let winWs = null if (CompositorService.isNiri) { winWs = w.workspace_id + } else if (CompositorService.isSway) { + winWs = w.workspace?.num } else { - // For Hyprland, we need to find the corresponding Hyprland toplevel to get workspace const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []) const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w) winWs = hyprToplevel?.workspace?.id @@ -132,14 +166,14 @@ Item { "type": "icon", "icon": icon, "isSteamApp": isSteamApp, - "active": !!(w.activated || (CompositorService.isNiri && w.is_focused)), + "active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)), "count": 1, "windowId": w.address || w.id, "fallbackText": w.appId || w.class || w.title || "" } } else { byApp[key].count++ - if (w.activated || (CompositorService.isNiri && w.is_focused)) { + if ((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)) { byApp[key].active = true } } @@ -155,6 +189,8 @@ Item { placeholder = {"id": -1, "name": ""} } else if (CompositorService.isDwl) { placeholder = {"tag": -1} + } else if (CompositorService.isSway) { + placeholder = {"num": -1} } else { placeholder = -1 } @@ -277,6 +313,8 @@ Item { return ws && ws.id !== -1 } else if (CompositorService.isDwl) { return ws && ws.tag !== -1 + } else if (CompositorService.isSway) { + return ws && ws.num !== -1 } return ws !== -1 }) @@ -328,12 +366,27 @@ Item { } DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag) + } else if (CompositorService.isSway) { + const realWorkspaces = getRealWorkspaces() + if (realWorkspaces.length < 2) { + return + } + + const currentIndex = realWorkspaces.findIndex(ws => ws.num === root.currentWorkspace) + const validIndex = currentIndex === -1 ? 0 : currentIndex + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0) + + if (nextIndex === validIndex) { + return + } + + try { I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`) } catch(_){} } } width: isVertical ? barThickness : visualWidth height: isVertical ? visualHeight : barThickness - visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl + visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway Rectangle { id: visualBackground @@ -378,6 +431,8 @@ Item { return modelData && modelData.id === root.currentWorkspace } else if (CompositorService.isDwl) { return modelData && modelData.tag === root.currentWorkspace + } else if (CompositorService.isSway) { + return modelData && modelData.num === root.currentWorkspace } return modelData === root.currentWorkspace } @@ -386,6 +441,8 @@ Item { return modelData && modelData.id === -1 } else if (CompositorService.isDwl) { return modelData && modelData.tag === -1 + } else if (CompositorService.isSway) { + return modelData && modelData.num === -1 } return modelData === -1 } @@ -400,6 +457,8 @@ Item { return loadedIsUrgent } else if (CompositorService.isDwl) { return modelData?.state === 2 + } else if (CompositorService.isSway) { + return loadedIsUrgent } return false } @@ -452,6 +511,8 @@ Item { Hyprland.dispatch(`workspace ${modelData.id}`) } else if (CompositorService.isDwl && modelData?.tag !== undefined) { DwlService.switchToTag(root.screenName, modelData.tag) + } else if (CompositorService.isSway && modelData?.num) { + try { I3.dispatch(`workspace number ${modelData.num}`) } catch(_){} } } } @@ -476,9 +537,11 @@ Item { wsData = modelData; } else if (CompositorService.isDwl) { wsData = modelData; + } else if (CompositorService.isSway) { + wsData = modelData; } delegateRoot.loadedWorkspaceData = wsData; - delegateRoot.loadedIsUrgent = wsData?.is_urgent ?? false; + delegateRoot.loadedIsUrgent = wsData?.urgent ?? false; var icData = null; if (wsData?.name) { @@ -488,7 +551,7 @@ Item { delegateRoot.loadedHasIcon = icData !== null; if (SettingsData.showWorkspaceApps) { - if (CompositorService.isDwl) { + if (CompositorService.isDwl || CompositorService.isSway) { delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData); } else { delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData)); @@ -742,6 +805,8 @@ Item { isPlaceholder = modelData?.id === -1 } else if (CompositorService.isDwl) { isPlaceholder = modelData?.tag === -1 + } else if (CompositorService.isSway) { + isPlaceholder = modelData?.num === -1 } else { isPlaceholder = modelData === -1 } @@ -754,6 +819,8 @@ Item { return modelData?.id || "" } else if (CompositorService.isDwl) { return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "" + } else if (CompositorService.isSway) { + return modelData?.num || "" } return modelData - 1 } @@ -788,6 +855,11 @@ Item { enabled: CompositorService.isDwl function onStateChanged() { delegateRoot.updateAllData() } } + Connections { + target: I3.workspaces + enabled: CompositorService.isSway + function onValuesChanged() { delegateRoot.updateAllData() } + } } } } diff --git a/Modules/Settings/LauncherTab.qml b/Modules/Settings/LauncherTab.qml index 16112d19..a901024c 100644 --- a/Modules/Settings/LauncherTab.qml +++ b/Modules/Settings/LauncherTab.qml @@ -93,6 +93,8 @@ Item { modes.push("Hyprland") } else if (CompositorService.isDwl) { modes.push("mango") + } else if (CompositorService.isSway) { + modes.push("Sway") } else { modes.push(I18n.tr("Compositor")) } diff --git a/README.md b/README.md index 611966cc..ce9fb3f1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ -A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/). Optimized for the [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), and [dwl/mangowc](https://github.com/DreamMaoMao/mangowc) compositors. +A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/). Optimized for the [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [sway](https://swaywm.org/), and [dwl/mangowc](https://github.com/DreamMaoMao/mangowc) compositors. Features notifications, app launcher, wallpaper customization, and fully customizable with [plugins](https://github.com/AvengeMedia/dms-plugin-registry). @@ -134,7 +134,7 @@ curl -fsSL https://install.danklinux.com | sh ### Compositor Setup -DankMaterialShell particularly aims at supporting the **niri** and **Hyprland** compositors, but it does support more wayland compositors with a diminished feature set (no monitor off, workspace switcher, overview integration, etc.): +DankMaterialShell particularly aims at supporting the **niri**, **Hyprland**, **sway**, and **dwl/MangoWC** compositors, but it does support more wayland compositors with a diminished feature set (no monitor off, workspace switcher, overview integration, etc.): **Niri**: ```bash @@ -164,6 +164,10 @@ sudo dnf copr enable solopasha/hyprland && sudo dnf install hyprland For detailed Hyprland installation instructions, see the [Hyprland wiki](https://wiki.hypr.land/Getting-Started/Installation/). +**sway/dwl (MangoWC)**: + +TODO - not documented. + ### Dank Shell Installation *feel free to contribute steps for other distributions* diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index e1a82f8d..71362890 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -13,10 +13,12 @@ Singleton { property bool isHyprland: false property bool isNiri: false property bool isDwl: false + property bool isSway: false property string compositor: "unknown" readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") readonly property string niriSocket: Quickshell.env("NIRI_SOCKET") + readonly property string swaySocket: Quickshell.env("SWAYSOCK") property bool useNiriSorting: isNiri && NiriService property var sortedToplevels: sortedToplevelsCache @@ -363,6 +365,7 @@ Singleton { isHyprland = true isNiri = false isDwl = false + isSway = false compositor = "hyprland" console.info("CompositorService: Detected Hyprland") try { @@ -377,28 +380,39 @@ Singleton { isNiri = true isHyprland = false isDwl = false + isSway = false compositor = "niri" console.info("CompositorService: Detected Niri with socket:", niriSocket) NiriService.generateNiriBinds() NiriService.generateNiriBlurrule() - } else { - isHyprland = false - isNiri = true - isDwl = false - compositor = "niri" - console.warn("CompositorService: Niri socket check failed, defaulting to Niri anyway") } }, 0) + return + } + + if (swaySocket && swaySocket.length > 0) { + Proc.runCommand("swaySocketCheck", ["test", "-S", swaySocket], (output, exitCode) => { + if (exitCode === 0) { + isNiri = false + isHyprland = false + isDwl = false + isSway = true + compositor = "sway" + console.info("CompositorService: Detected Sway with socket:", swaySocket) + } + }, 0) + return + } + + if (DMSService.dmsAvailable) { + Qt.callLater(checkForDwl) } else { - if (DMSService.dmsAvailable) { - Qt.callLater(checkForDwl) - } else { - isHyprland = false - isNiri = false - isDwl = false - compositor = "unknown" - console.warn("CompositorService: No compositor detected") - } + isHyprland = false + isNiri = false + isDwl = false + isSway = false + compositor = "unknown" + console.warn("CompositorService: No compositor detected") } } @@ -425,6 +439,7 @@ Singleton { if (isNiri) return NiriService.powerOffMonitors() if (isHyprland) return Hyprland.dispatch("dpms off") if (isDwl) return _dwlPowerOffMonitors() + if (isSway) { try { I3.dispatch("output * dpms off") } catch(_){} return } console.warn("CompositorService: Cannot power off monitors, unknown compositor") } @@ -432,6 +447,7 @@ Singleton { if (isNiri) return NiriService.powerOnMonitors() if (isHyprland) return Hyprland.dispatch("dpms on") if (isDwl) return _dwlPowerOnMonitors() + if (isSway) { try { I3.dispatch("output * dpms on") } catch(_){} return } console.warn("CompositorService: Cannot power on monitors, unknown compositor") } diff --git a/Services/SessionService.qml b/Services/SessionService.qml index b34e8ce1..00929098 100644 --- a/Services/SessionService.qml +++ b/Services/SessionService.qml @@ -6,6 +6,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Hyprland +import Quickshell.I3 import Quickshell.Wayland import qs.Common @@ -189,6 +190,11 @@ Singleton { return } + if (CompositorService.isSway) { + try { I3.dispatch("exit") } catch(_){} + return + } + Hyprland.dispatch("exit") } else { Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionLogout]) diff --git a/assets/sway.svg b/assets/sway.svg new file mode 100644 index 00000000..96397c5e --- /dev/null +++ b/assets/sway.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +