From f94011cf05a47592c071acb32c418d50e30b4482 Mon Sep 17 00:00:00 2001 From: Varshit Date: Tue, 9 Dec 2025 21:57:46 +0100 Subject: [PATCH] feat: add scroll compositor support (#959) * added scroll support * import QuickShell.i3 * update scroll provider registration logic * improve scroll support for workspace switcher * update title for scroll keybinds * add scroll to dms-greeter * fix: formatting & sway keybind provider * readme update --------- Co-authored-by: bbedward --- README.md | 13 +- core/cmd/dms/commands_keybinds.go | 7 + core/internal/keybinds/providers/sway.go | 28 ++- core/internal/screenshot/compositor.go | 29 ++++ distro/nix/greeter.nix | 2 +- quickshell/Modules/DankBar/DankBar.qml | 4 +- quickshell/Modules/DankBar/DankBarContent.qml | 74 ++++---- .../DankBar/Widgets/LauncherButton.qml | 25 +-- .../DankBar/Widgets/WorkspaceSwitcher.qml | 36 ++-- .../DankDash/Overview/UserInfoCard.qml | 2 + quickshell/Modules/Greetd/assets/dms-greeter | 22 ++- quickshell/Modules/Settings/AboutTab.qml | 13 +- quickshell/Modules/Settings/LauncherTab.qml | 2 + quickshell/Services/BarWidgetService.qml | 3 +- quickshell/Services/CompositorService.qml | 35 +++- quickshell/Services/DwlService.qml | 162 +++++++++--------- quickshell/Services/ExtWorkspaceService.qml | 2 +- quickshell/Services/SessionService.qml | 8 +- 18 files changed, 298 insertions(+), 169 deletions(-) diff --git a/README.md b/README.md index e7529c18..3fd7e96f 100644 --- a/README.md +++ b/README.md @@ -5,21 +5,21 @@ DankMaterialShell - ### A modern desktop shell for Wayland +### A modern desktop shell for Wayland - Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/) +Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/) [![Documentation](https://img.shields.io/badge/docs-danklinux.com-9ccbfb?style=for-the-badge&labelColor=101418)](https://danklinux.com/docs) [![GitHub stars](https://img.shields.io/github/stars/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=ffd700)](https://github.com/AvengeMedia/DankMaterialShell/stargazers) [![GitHub License](https://img.shields.io/github/license/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=b9c8da)](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE) [![GitHub release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases) [![AUR version](https://img.shields.io/aur/version/dms-shell-bin?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://aur.archlinux.org/packages/dms-shell-bin) -[![AUR version (git)](https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git))](https://aur.archlinux.org/packages/dms-shell-git) +[![AUR version (git)]()](https://aur.archlinux.org/packages/dms-shell-git) [![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Fdanklinux)](https://ko-fi.com/danklinux) -DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop. +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. ## 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), and [labwc](https://labwc.github.io/) 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/), and [Scroll](https://github.com/dawsers/scroll) 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) @@ -143,6 +143,7 @@ See component-specific documentation: ### Building from Source **Core + Dankinstall:** + ```bash cd core make # Build dms CLI @@ -150,11 +151,13 @@ make dankinstall # Build installer ``` **Shell:** + ```bash quickshell -p quickshell/ ``` **NixOS:** + ```nix { inputs.dms.url = "github:AvengeMedia/DankMaterialShell"; diff --git a/core/cmd/dms/commands_keybinds.go b/core/cmd/dms/commands_keybinds.go index 0e90b2f3..c7611c4f 100644 --- a/core/cmd/dms/commands_keybinds.go +++ b/core/cmd/dms/commands_keybinds.go @@ -89,6 +89,11 @@ func initializeProviders() { log.Warnf("Failed to register MangoWC provider: %v", err) } + scrollProvider := providers.NewSwayProvider("$HOME/.config/scroll") + if err := registry.Register(scrollProvider); err != nil { + log.Warnf("Failed to register Scroll 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) @@ -125,6 +130,8 @@ func makeProviderWithPath(name, path string) keybinds.Provider { return providers.NewMangoWCProvider(path) case "sway": return providers.NewSwayProvider(path) + case "scroll": + return providers.NewSwayProvider(path) case "niri": return providers.NewNiriProvider(path) default: diff --git a/core/internal/keybinds/providers/sway.go b/core/internal/keybinds/providers/sway.go index 811dff9c..f2f817ce 100644 --- a/core/internal/keybinds/providers/sway.go +++ b/core/internal/keybinds/providers/sway.go @@ -2,6 +2,7 @@ package providers import ( "fmt" + "os" "strings" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" @@ -9,18 +10,38 @@ import ( type SwayProvider struct { configPath string + isScroll bool } func NewSwayProvider(configPath string) *SwayProvider { + isScroll := false + _, ok := os.LookupEnv("SCROLLSOCK") + if ok { + isScroll = true + } if configPath == "" { configPath = "$HOME/.config/sway" } + if isScroll && configPath == "" { + configPath = "$HOME/.config/scroll" + } return &SwayProvider{ configPath: configPath, + isScroll: isScroll, } } func (s *SwayProvider) Name() string { + if s != nil && s.isScroll { + return "scroll" + } + if s == nil { + _, ok := os.LookupEnv("SCROLLSOCK") + if ok { + return "scroll" + } + } + return "sway" } @@ -33,8 +54,13 @@ func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { categorizedBinds := make(map[string][]keybinds.Keybind) s.convertSection(section, "", categorizedBinds) + cheatSheetTitle := "Sway Keybinds" + if s != nil && s.isScroll { + cheatSheetTitle = "Scroll Keybinds" + } + return &keybinds.CheatSheet{ - Title: "Sway Keybinds", + Title: cheatSheetTitle, Provider: s.Name(), Binds: categorizedBinds, }, nil diff --git a/core/internal/screenshot/compositor.go b/core/internal/screenshot/compositor.go index 64b0a8d3..79e7d007 100644 --- a/core/internal/screenshot/compositor.go +++ b/core/internal/screenshot/compositor.go @@ -20,6 +20,7 @@ const ( CompositorSway CompositorNiri CompositorDWL + CompositorScroll ) var detectedCompositor Compositor = -1 @@ -32,6 +33,7 @@ func DetectCompositor() Compositor { hyprlandSig := os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") niriSocket := os.Getenv("NIRI_SOCKET") swaySocket := os.Getenv("SWAYSOCK") + scrollSocket := os.Getenv("SCROLLSOCK") switch { case niriSocket != "": @@ -39,6 +41,12 @@ func DetectCompositor() Compositor { detectedCompositor = CompositorNiri return detectedCompositor } + case scrollSocket != "": + if _, err := os.Stat(scrollSocket); err == nil { + detectedCompositor = CompositorScroll + return detectedCompositor + } + case swaySocket != "": if _, err := os.Stat(swaySocket); err == nil { detectedCompositor = CompositorSway @@ -233,6 +241,25 @@ func getSwayFocusedMonitor() string { return "" } +func getScrollFocusedMonitor() string { + output, err := exec.Command("scrollmsg", "-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"` @@ -378,6 +405,8 @@ func GetFocusedMonitor() string { return getHyprlandFocusedMonitor() case CompositorSway: return getSwayFocusedMonitor() + case CompositorScroll: + return getScrollFocusedMonitor() case CompositorNiri: return getNiriFocusedMonitor() case CompositorDWL: diff --git a/distro/nix/greeter.nix b/distro/nix/greeter.nix index 79c0dd4a..7b306398 100644 --- a/distro/nix/greeter.nix +++ b/distro/nix/greeter.nix @@ -38,7 +38,7 @@ in { options.programs.dankMaterialShell.greeter = { enable = lib.mkEnableOption "DankMaterialShell greeter"; compositor.name = lib.mkOption { - type = types.enum ["niri" "hyprland" "sway"]; + type = types.enum ["niri" "hyprland" "sway" "scroll"]; description = "Compositor to run greeter in"; }; compositor.customConfig = lib.mkOption { diff --git a/quickshell/Modules/DankBar/DankBar.qml b/quickshell/Modules/DankBar/DankBar.qml index e1b37f98..4f58268b 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) { + } else if (CompositorService.isSway || CompositorService.isScroll) { const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); focusedScreenName = focusedWs?.monitor?.name || ""; } @@ -123,7 +123,7 @@ Item { focusedScreenName = Hyprland.focusedWorkspace.monitor.name; } else if (CompositorService.isNiri && NiriService.currentOutput) { focusedScreenName = NiriService.currentOutput; - } else if (CompositorService.isSway) { + } else if (CompositorService.isSway || CompositorService.isScroll) { const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); focusedScreenName = focusedWs?.monitor?.name || ""; } diff --git a/quickshell/Modules/DankBar/DankBarContent.qml b/quickshell/Modules/DankBar/DankBarContent.qml index 79e3f1ee..90230d0a 100644 --- a/quickshell/Modules/DankBar/DankBarContent.qml +++ b/quickshell/Modules/DankBar/DankBarContent.qml @@ -82,7 +82,7 @@ Item { }, (_, i) => i); } return DwlService.getVisibleTags(barWindow.screenName); - } else if (CompositorService.isSway) { + } else if (CompositorService.isSway || CompositorService.isScroll) { const workspaces = I3.workspaces?.values || []; if (workspaces.length === 0) return [ @@ -124,7 +124,7 @@ Item { return 0; const activeTags = DwlService.getActiveTags(barWindow.screenName); return activeTags.length > 0 ? activeTags[0] : 0; - } else if (CompositorService.isSway) { + } else if (CompositorService.isSway || CompositorService.isScroll) { if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) { const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); return focusedWs ? focusedWs.num : 1; @@ -169,7 +169,7 @@ Item { if (nextIndex !== validIndex) { DwlService.switchToTag(barWindow.screenName, realWorkspaces[nextIndex]); } - } else if (CompositorService.isSway) { + } else if (CompositorService.isSway || CompositorService.isScroll) { const currentWs = getCurrentWorkspace(); const currentIndex = realWorkspaces.findIndex(ws => ws.num === currentWs); const validIndex = currentIndex === -1 ? 0 : currentIndex; @@ -315,37 +315,37 @@ Item { } readonly property var allComponents: ({ - "launcherButtonComponent": launcherButtonComponent, - "workspaceSwitcherComponent": workspaceSwitcherComponent, - "focusedWindowComponent": focusedWindowComponent, - "runningAppsComponent": runningAppsComponent, - "clockComponent": clockComponent, - "mediaComponent": mediaComponent, - "weatherComponent": weatherComponent, - "systemTrayComponent": systemTrayComponent, - "privacyIndicatorComponent": privacyIndicatorComponent, - "clipboardComponent": clipboardComponent, - "cpuUsageComponent": cpuUsageComponent, - "memUsageComponent": memUsageComponent, - "diskUsageComponent": diskUsageComponent, - "cpuTempComponent": cpuTempComponent, - "gpuTempComponent": gpuTempComponent, - "notificationButtonComponent": notificationButtonComponent, - "batteryComponent": batteryComponent, - "layoutComponent": layoutComponent, - "controlCenterButtonComponent": controlCenterButtonComponent, - "capsLockIndicatorComponent": capsLockIndicatorComponent, - "idleInhibitorComponent": idleInhibitorComponent, - "spacerComponent": spacerComponent, - "separatorComponent": separatorComponent, - "networkComponent": networkComponent, - "keyboardLayoutNameComponent": keyboardLayoutNameComponent, - "vpnComponent": vpnComponent, - "notepadButtonComponent": notepadButtonComponent, - "colorPickerComponent": colorPickerComponent, - "systemUpdateComponent": systemUpdateComponent, - "powerMenuButtonComponent": powerMenuButtonComponent - }) + "launcherButtonComponent": launcherButtonComponent, + "workspaceSwitcherComponent": workspaceSwitcherComponent, + "focusedWindowComponent": focusedWindowComponent, + "runningAppsComponent": runningAppsComponent, + "clockComponent": clockComponent, + "mediaComponent": mediaComponent, + "weatherComponent": weatherComponent, + "systemTrayComponent": systemTrayComponent, + "privacyIndicatorComponent": privacyIndicatorComponent, + "clipboardComponent": clipboardComponent, + "cpuUsageComponent": cpuUsageComponent, + "memUsageComponent": memUsageComponent, + "diskUsageComponent": diskUsageComponent, + "cpuTempComponent": cpuTempComponent, + "gpuTempComponent": gpuTempComponent, + "notificationButtonComponent": notificationButtonComponent, + "batteryComponent": batteryComponent, + "layoutComponent": layoutComponent, + "controlCenterButtonComponent": controlCenterButtonComponent, + "capsLockIndicatorComponent": capsLockIndicatorComponent, + "idleInhibitorComponent": idleInhibitorComponent, + "spacerComponent": spacerComponent, + "separatorComponent": separatorComponent, + "networkComponent": networkComponent, + "keyboardLayoutNameComponent": keyboardLayoutNameComponent, + "vpnComponent": vpnComponent, + "notepadButtonComponent": notepadButtonComponent, + "colorPickerComponent": colorPickerComponent, + "systemUpdateComponent": systemUpdateComponent, + "powerMenuButtonComponent": powerMenuButtonComponent + }) Item { id: stackContainer @@ -534,7 +534,7 @@ Item { section: topBarContent.getWidgetSection(parent) parentScreen: barWindow.screen onClicked: { - clipboardHistoryModalPopup.toggle() + clipboardHistoryModalPopup.toggle(); } } } @@ -550,9 +550,9 @@ Item { parentScreen: barWindow.screen onClicked: { if (powerMenuModalLoader) { - powerMenuModalLoader.active = true + powerMenuModalLoader.active = true; if (powerMenuModalLoader.item) { - powerMenuModalLoader.item.openCentered() + powerMenuModalLoader.item.openCentered(); } } } diff --git a/quickshell/Modules/DankBar/Widgets/LauncherButton.qml b/quickshell/Modules/DankBar/Widgets/LauncherButton.qml index ad140a97..15bf3523 100644 --- a/quickshell/Modules/DankBar/Widgets/LauncherButton.qml +++ b/quickshell/Modules/DankBar/Widgets/LauncherButton.qml @@ -1,6 +1,5 @@ import QtQuick import QtQuick.Effects -import Quickshell import Quickshell.Widgets import qs.Common import qs.Modules.Plugins @@ -56,7 +55,7 @@ BasePill { } IconImage { - visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isLabwc) + visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isLabwc) anchors.centerIn: parent width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset) height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset) @@ -64,17 +63,19 @@ BasePill { asynchronous: true source: { if (CompositorService.isNiri) { - return "file://" + Theme.shellDir + "/assets/niri.svg" + return "file://" + Theme.shellDir + "/assets/niri.svg"; } else if (CompositorService.isHyprland) { - return "file://" + Theme.shellDir + "/assets/hyprland.svg" + return "file://" + Theme.shellDir + "/assets/hyprland.svg"; } else if (CompositorService.isDwl) { - return "file://" + Theme.shellDir + "/assets/mango.png" + return "file://" + Theme.shellDir + "/assets/mango.png"; } else if (CompositorService.isSway) { - return "file://" + Theme.shellDir + "/assets/sway.svg" + return "file://" + Theme.shellDir + "/assets/sway.svg"; + } else if (CompositorService.isScroll) { + return "file://" + Theme.shellDir + "/assets/sway.svg"; } else if (CompositorService.isLabwc) { - return "file://" + Theme.shellDir + "/assets/labwc.png" + return "file://" + Theme.shellDir + "/assets/labwc.png"; } - return "" + return ""; } layer.enabled: Theme.effectiveLogoColor !== "" layer.effect: MultiEffect { @@ -82,10 +83,10 @@ BasePill { colorization: 1 colorizationColor: Theme.effectiveLogoColor brightness: { - SettingsData.launcherLogoBrightness + SettingsData.launcherLogoBrightness; } contrast: { - SettingsData.launcherLogoContrast + SettingsData.launcherLogoContrast; } } } @@ -112,9 +113,9 @@ BasePill { onRightClicked: { if (CompositorService.isNiri) { - NiriService.toggleOverview() + NiriService.toggleOverview(); } else if (root.hyprlandOverviewLoader?.item) { - root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen + root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen; } } } diff --git a/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml b/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml index 77b1a6ff..edf9cc4f 100644 --- a/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml +++ b/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml @@ -23,7 +23,7 @@ Item { return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); } - readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && ExtWorkspaceService.extWorkspaceAvailable) + readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && ExtWorkspaceService.extWorkspaceAvailable) Connections { target: DesktopEntries @@ -45,6 +45,7 @@ Item { const activeTags = getDwlActiveTags(); return activeTags.length > 0 ? activeTags[0] : -1; case "sway": + case "scroll": return getSwayActiveWorkspace(); default: return 1; @@ -74,6 +75,7 @@ Item { baseList = getDwlTags(); break; case "sway": + case "scroll": baseList = getSwayWorkspaces(); break; default: @@ -192,7 +194,7 @@ Item { return []; } targetWorkspaceId = ws.tag; - } else if (CompositorService.isSway) { + } else if (CompositorService.isSway || CompositorService.isScroll) { targetWorkspaceId = ws.num !== undefined ? ws.num : ws; } else { return []; @@ -204,7 +206,7 @@ Item { let isActiveWs = false; if (CompositorService.isNiri) { isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active); - } else if (CompositorService.isSway) { + } else if (CompositorService.isSway || CompositorService.isScroll) { const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false; } else if (CompositorService.isDwl) { @@ -225,7 +227,7 @@ Item { let winWs = null; if (CompositorService.isNiri) { winWs = w.workspace_id; - } else if (CompositorService.isSway) { + } else if (CompositorService.isSway || CompositorService.isScroll) { winWs = w.workspace?.num; } else { const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []); @@ -283,7 +285,7 @@ Item { placeholder = { "tag": -1 }; - } else if (CompositorService.isSway) { + } else if (CompositorService.isSway || CompositorService.isScroll) { placeholder = { "num": -1 }; @@ -453,7 +455,7 @@ Item { return ws && ws.id !== -1; if (CompositorService.isDwl) return ws && ws.tag !== -1; - if (CompositorService.isSway) + if (CompositorService.isSway || CompositorService.isScroll) return ws && ws.num !== -1; return ws !== -1; }); @@ -521,7 +523,7 @@ Item { } DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag); - } else if (CompositorService.isSway) { + } else if (CompositorService.isSway || CompositorService.isScroll) { const realWorkspaces = getRealWorkspaces(); if (realWorkspaces.length < 2) { return; @@ -549,7 +551,7 @@ Item { isPlaceholder = modelData?.id === -1; } else if (CompositorService.isDwl) { isPlaceholder = modelData?.tag === -1; - } else if (CompositorService.isSway) { + } else if (CompositorService.isSway || CompositorService.isScroll) { isPlaceholder = modelData?.num === -1; } else { isPlaceholder = modelData === -1; @@ -564,12 +566,12 @@ Item { return modelData?.id || ""; if (CompositorService.isDwl) return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""; - if (CompositorService.isSway) + if (CompositorService.isSway || CompositorService.isScroll) return modelData?.num || ""; return modelData - 1; } - readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway + readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll readonly property bool hasWorkspaces: getRealWorkspaces().length > 0 readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces) @@ -675,7 +677,7 @@ Item { return !!(modelData && modelData.id === root.currentWorkspace); if (CompositorService.isDwl) return !!(modelData && root.dwlActiveTags.includes(modelData.tag)); - if (CompositorService.isSway) + if (CompositorService.isSway || CompositorService.isScroll) return !!(modelData && modelData.num === root.currentWorkspace); return modelData === root.currentWorkspace; } @@ -686,7 +688,7 @@ Item { return !!(modelData && modelData.id === -1); if (CompositorService.isDwl) return !!(modelData && modelData.tag === -1); - if (CompositorService.isSway) + if (CompositorService.isSway || CompositorService.isScroll) return !!(modelData && modelData.num === -1); return modelData === -1; } @@ -703,7 +705,7 @@ Item { return loadedIsUrgent; if (CompositorService.isDwl) return modelData?.state === 2; - if (CompositorService.isSway) + if (CompositorService.isSway || CompositorService.isScroll) return loadedIsUrgent; return false; } @@ -767,7 +769,7 @@ Item { console.log("Calling switchToTag"); DwlService.switchToTag(root.screenName, modelData.tag); } - } else if (CompositorService.isSway && modelData?.num) { + } else if ((CompositorService.isSway || CompositorService.isScroll) && modelData?.num) { try { I3.dispatch(`workspace number ${modelData.num}`); } catch (_) {} @@ -797,7 +799,7 @@ Item { wsData = modelData; } else if (CompositorService.isDwl) { wsData = modelData; - } else if (CompositorService.isSway) { + } else if (CompositorService.isSway || CompositorService.isScroll) { wsData = modelData; } delegateRoot.loadedWorkspaceData = wsData; @@ -811,7 +813,7 @@ Item { delegateRoot.loadedHasIcon = icData !== null; if (SettingsData.showWorkspaceApps) { - if (CompositorService.isDwl || CompositorService.isSway) { + if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll) { delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData); } else { delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData)); @@ -1192,7 +1194,7 @@ Item { } Connections { target: I3.workspaces - enabled: CompositorService.isSway + enabled: (CompositorService.isSway || CompositorService.isScroll) function onValuesChanged() { delegateRoot.updateAllData(); } diff --git a/quickshell/Modules/DankDash/Overview/UserInfoCard.qml b/quickshell/Modules/DankDash/Overview/UserInfoCard.qml index cae6c1cf..ff582f99 100644 --- a/quickshell/Modules/DankDash/Overview/UserInfoCard.qml +++ b/quickshell/Modules/DankDash/Overview/UserInfoCard.qml @@ -67,6 +67,8 @@ Card { return "on MangoWC"; if (CompositorService.isSway) return "on Sway"; + if (CompositorService.isScroll) + return "on Scroll"; return ""; } font.pixelSize: Theme.fontSizeSmall diff --git a/quickshell/Modules/Greetd/assets/dms-greeter b/quickshell/Modules/Greetd/assets/dms-greeter index 38894744..4e6698cc 100755 --- a/quickshell/Modules/Greetd/assets/dms-greeter +++ b/quickshell/Modules/Greetd/assets/dms-greeter @@ -16,7 +16,7 @@ dms-greeter - DankMaterialShell greeter launcher Usage: dms-greeter --command COMPOSITOR [OPTIONS] Required: - --command COMPOSITOR Compositor to use (niri, hyprland, sway, or mangowc) + --command COMPOSITOR Compositor to use (niri, hyprland, sway, scroll or mangowc) Options: -C, --config PATH Custom compositor config file @@ -30,6 +30,7 @@ Examples: dms-greeter --command niri dms-greeter --command hyprland -C /etc/greetd/custom-hypr.conf dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms + dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms dms-greeter --command niri --cache-dir /tmp/dmsgreeter dms-greeter --command mangowc EOF @@ -207,6 +208,25 @@ SWAY_EOF exec sway -c "$COMPOSITOR_CONFIG" ;; + scroll) + if [[ -z "$COMPOSITOR_CONFIG" ]]; then + TEMP_CONFIG=$(mktemp) + cat > "$TEMP_CONFIG" << SCROLL_EOF +exec "$QS_CMD; scrollmsg exit" +SCROLL_EOF + COMPOSITOR_CONFIG="$TEMP_CONFIG" + else + TEMP_CONFIG=$(mktemp) + cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG" + cat >> "$TEMP_CONFIG" << SCROLL_EOF + +exec "$QS_CMD; scrollmsg exit" +SCROLL_EOF + COMPOSITOR_CONFIG="$TEMP_CONFIG" + fi + exec scroll -c "$COMPOSITOR_CONFIG" + ;; + mangowc) if [[ -n "$COMPOSITOR_CONFIG" ]]; then exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit" diff --git a/quickshell/Modules/Settings/AboutTab.qml b/quickshell/Modules/Settings/AboutTab.qml index 0f560714..fe87e0e4 100644 --- a/quickshell/Modules/Settings/AboutTab.qml +++ b/quickshell/Modules/Settings/AboutTab.qml @@ -10,6 +10,7 @@ Item { property bool isHyprland: CompositorService.isHyprland property bool isNiri: CompositorService.isNiri property bool isSway: CompositorService.isSway + property bool isScroll: CompositorService.isScroll property bool isDwl: CompositorService.isDwl property bool isLabwc: CompositorService.isLabwc @@ -18,6 +19,8 @@ Item { return "hyprland"; if (isSway) return "sway"; + if (isScroll) + return "scroll"; if (isDwl) return "mangowc"; if (isLabwc) @@ -30,6 +33,8 @@ Item { return "/assets/hyprland.svg"; if (isSway) return "/assets/sway.svg"; + if (isScroll) + return "/assets/sway.svg"; if (isDwl) return "/assets/mango.png"; if (isLabwc) @@ -42,6 +47,8 @@ Item { return "https://hypr.land"; if (isSway) return "https://swaywm.org"; + if (isScroll) + return "https://github.com/dawsers/scroll"; if (isDwl) return "https://github.com/DreamMaoMao/mangowc"; if (isLabwc) @@ -54,6 +61,8 @@ Item { return "Hyprland Website"; if (isSway) return "Sway Website"; + if (isScroll) + return "Scroll Github"; if (isDwl) return "mangowc GitHub"; if (isLabwc) @@ -86,9 +95,9 @@ Item { property string ircUrl: "https://web.libera.chat/gamja/?channels=#labwc" property string ircTooltip: "LabWC IRC Channel" - property bool showMatrix: isNiri && !isHyprland && !isSway && !isDwl && !isLabwc + property bool showMatrix: isNiri && !isHyprland && !isSway && !isScroll && !isDwl && !isLabwc property bool showCompositorDiscord: isHyprland || isDwl - property bool showReddit: isNiri && !isHyprland && !isSway && !isDwl && !isLabwc + property bool showReddit: isNiri && !isHyprland && !isSway && !isScroll && !isDwl && !isLabwc property bool showIrc: isLabwc DankFlickable { diff --git a/quickshell/Modules/Settings/LauncherTab.qml b/quickshell/Modules/Settings/LauncherTab.qml index b93a8184..c07683b8 100644 --- a/quickshell/Modules/Settings/LauncherTab.qml +++ b/quickshell/Modules/Settings/LauncherTab.qml @@ -63,6 +63,8 @@ Item { modes.push("mango"); } else if (CompositorService.isSway) { modes.push("Sway"); + } else if (CompositorService.isScroll) { + modes.push("Scroll"); } else { modes.push(I18n.tr("Compositor")); } diff --git a/quickshell/Services/BarWidgetService.qml b/quickshell/Services/BarWidgetService.qml index de56fbb8..3a0c18fa 100644 --- a/quickshell/Services/BarWidgetService.qml +++ b/quickshell/Services/BarWidgetService.qml @@ -1,5 +1,6 @@ pragma Singleton pragma ComponentBehavior: Bound + import QtQuick import Quickshell import Quickshell.Hyprland @@ -65,7 +66,7 @@ Singleton { return Hyprland.focusedWorkspace.monitor.name; if (CompositorService.isNiri && NiriService.currentOutput) return NiriService.currentOutput; - if (CompositorService.isSway) { + if (CompositorService.isSway || CompositorService.isScroll) { 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 29335bc3..dd7ecf57 100644 --- a/quickshell/Services/CompositorService.qml +++ b/quickshell/Services/CompositorService.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Quickshell.I3 import Quickshell.Wayland import Quickshell.Hyprland import qs.Common @@ -14,6 +15,7 @@ Singleton { property bool isNiri: false property bool isDwl: false property bool isSway: false + property bool isScroll: false property bool isLabwc: false property string compositor: "unknown" readonly property bool useHyprlandFocusGrab: isHyprland && Quickshell.env("DMS_HYPRLAND_EXCLUSIVE_FOCUS") !== "1" @@ -21,6 +23,7 @@ Singleton { readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") 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 labwcPid: Quickshell.env("LABWC_PID") property bool useNiriSorting: isNiri && NiriService @@ -71,7 +74,7 @@ Singleton { screenName = Hyprland.focusedWorkspace.monitor.name; else if (isNiri && NiriService.currentOutput) screenName = NiriService.currentOutput; - else if (isSway) { + else if (isSway || isScroll) { const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); screenName = focusedWs?.monitor?.name || ""; } else if (isDwl && DwlService.activeOutput) @@ -398,11 +401,12 @@ Singleton { } function detectCompositor() { - if (hyprlandSignature && hyprlandSignature.length > 0 && !niriSocket && !swaySocket && !labwcPid) { + if (hyprlandSignature && hyprlandSignature.length > 0 && !niriSocket && !swaySocket && !scrollSocket && !labwcPid) { isHyprland = true; isNiri = false; isDwl = false; isSway = false; + isScroll = false; isLabwc = false; compositor = "hyprland"; console.info("CompositorService: Detected Hyprland"); @@ -416,6 +420,7 @@ Singleton { isHyprland = false; isDwl = false; isSway = false; + isScroll = false; isLabwc = false; compositor = "niri"; console.info("CompositorService: Detected Niri with socket:", niriSocket); @@ -425,13 +430,14 @@ Singleton { return; } - if (swaySocket && swaySocket.length > 0) { + if (swaySocket && swaySocket.length > 0 && !scrollSocket && scrollSocket.length == 0) { Proc.runCommand("swaySocketCheck", ["test", "-S", swaySocket], (output, exitCode) => { if (exitCode === 0) { isNiri = false; isHyprland = false; isDwl = false; isSway = true; + isScroll = false; isLabwc = false; compositor = "sway"; console.info("CompositorService: Detected Sway with socket:", swaySocket); @@ -440,11 +446,28 @@ Singleton { return; } + if (scrollSocket && scrollSocket.length > 0) { + Proc.runCommand("scrollSocketCheck", ["test", "-S", scrollSocket], (output, exitCode) => { + if (exitCode === 0) { + isNiri = false; + isHyprland = false; + isDwl = false; + isSway = false; + isScroll = true; + isLabwc = false; + compositor = "scroll"; + console.info("CompositorService: Detected Scroll with socket:", scrollSocket); + } + }, 0); + return; + } + if (labwcPid && labwcPid.length > 0) { isHyprland = false; isNiri = false; isDwl = false; isSway = false; + isScroll = false; isLabwc = true; compositor = "labwc"; console.info("CompositorService: Detected LabWC with PID:", labwcPid); @@ -458,6 +481,7 @@ Singleton { isNiri = false; isDwl = false; isSway = false; + isScroll = false; isLabwc = false; compositor = "unknown"; console.warn("CompositorService: No compositor detected"); @@ -479,6 +503,7 @@ Singleton { isNiri = false; isDwl = true; isSway = false; + isScroll = false; isLabwc = false; compositor = "dwl"; console.info("CompositorService: Detected DWL via DMS capability"); @@ -492,7 +517,7 @@ Singleton { return Hyprland.dispatch("dpms off"); if (isDwl) return _dwlPowerOffMonitors(); - if (isSway) { + if (isSway || isScroll) { try { I3.dispatch("output * dpms off"); } catch (_) {} @@ -511,7 +536,7 @@ Singleton { return Hyprland.dispatch("dpms on"); if (isDwl) return _dwlPowerOnMonitors(); - if (isSway) { + if (isSway || isScroll) { try { I3.dispatch("output * dpms on"); } catch (_) {} diff --git a/quickshell/Services/DwlService.qml b/quickshell/Services/DwlService.qml index 592d20c8..d7c89b87 100644 --- a/quickshell/Services/DwlService.qml +++ b/quickshell/Services/DwlService.qml @@ -15,81 +15,82 @@ Singleton { property string activeOutput: "" property var outputScales: ({}) property string currentKeyboardLayout: { - if (!outputs || !activeOutput) return "" - const output = outputs[activeOutput] - return (output && output.kbLayout) || "" + if (!outputs || !activeOutput) + return ""; + const output = outputs[activeOutput]; + return (output && output.kbLayout) || ""; } - signal stateChanged() + signal stateChanged Connections { target: DMSService function onCapabilitiesReceived() { - checkCapabilities() + checkCapabilities(); } function onConnectionStateChanged() { if (DMSService.isConnected) { - checkCapabilities() + checkCapabilities(); } else { - dwlAvailable = false + dwlAvailable = false; } } function onDwlStateUpdate(data) { if (dwlAvailable) { - handleStateUpdate(data) + handleStateUpdate(data); } } } Component.onCompleted: { if (DMSService.dmsAvailable) { - checkCapabilities() + checkCapabilities(); } if (dwlAvailable) { - refreshOutputScales() + refreshOutputScales(); } } function checkCapabilities() { if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) { - dwlAvailable = false - return + dwlAvailable = false; + return; } - const hasDwl = DMSService.capabilities.includes("dwl") + const hasDwl = DMSService.capabilities.includes("dwl"); if (hasDwl && !dwlAvailable) { - dwlAvailable = true - console.info("DwlService: DWL capability detected") - requestState() - refreshOutputScales() + dwlAvailable = true; + console.info("DwlService: DWL capability detected"); + requestState(); + refreshOutputScales(); } else if (!hasDwl) { - dwlAvailable = false + dwlAvailable = false; } } function requestState() { if (!DMSService.isConnected || !dwlAvailable) { - return + return; } DMSService.sendRequest("dwl.getState", null, response => { if (response.result) { - handleStateUpdate(response.result) + handleStateUpdate(response.result); } - }) + }); } function handleStateUpdate(state) { - outputs = state.outputs || {} - tagCount = state.tagCount || 9 - layouts = state.layouts || [] - activeOutput = state.activeOutput || "" - stateChanged() + outputs = state.outputs || {}; + tagCount = state.tagCount || 9; + layouts = state.layouts || []; + activeOutput = state.activeOutput || ""; + stateChanged(); } function setTags(outputName, tagmask, toggleTagset) { if (!DMSService.isConnected || !dwlAvailable) { - return + return; } DMSService.sendRequest("dwl.setTags", { @@ -98,14 +99,14 @@ Singleton { "toggleTagset": toggleTagset }, response => { if (response.error) { - console.warn("DwlService: setTags error:", response.error) + console.warn("DwlService: setTags error:", response.error); } - }) + }); } function setClientTags(outputName, andTags, xorTags) { if (!DMSService.isConnected || !dwlAvailable) { - return + return; } DMSService.sendRequest("dwl.setClientTags", { @@ -114,14 +115,14 @@ Singleton { "xorTags": xorTags }, response => { if (response.error) { - console.warn("DwlService: setClientTags error:", response.error) + console.warn("DwlService: setClientTags error:", response.error); } - }) + }); } function setLayout(outputName, index) { if (!DMSService.isConnected || !dwlAvailable) { - return + return; } DMSService.sendRequest("dwl.setLayout", { @@ -129,77 +130,77 @@ Singleton { "index": index }, response => { if (response.error) { - console.warn("DwlService: setLayout error:", response.error) + console.warn("DwlService: setLayout error:", response.error); } - }) + }); } function getOutputState(outputName) { if (!outputs || !outputs[outputName]) { - return null + return null; } - return outputs[outputName] + return outputs[outputName]; } function getActiveTags(outputName) { - const output = getOutputState(outputName) + const output = getOutputState(outputName); if (!output || !output.tags) { - return [] + return []; } - return output.tags.filter(tag => tag.state === 1).map(tag => tag.tag) + return output.tags.filter(tag => tag.state === 1).map(tag => tag.tag); } function getTagsWithClients(outputName) { - const output = getOutputState(outputName) + const output = getOutputState(outputName); if (!output || !output.tags) { - return [] + return []; } - return output.tags.filter(tag => tag.clients > 0).map(tag => tag.tag) + return output.tags.filter(tag => tag.clients > 0).map(tag => tag.tag); } function getUrgentTags(outputName) { - const output = getOutputState(outputName) + const output = getOutputState(outputName); if (!output || !output.tags) { - return [] + return []; } - return output.tags.filter(tag => tag.state === 2).map(tag => tag.tag) + return output.tags.filter(tag => tag.state === 2).map(tag => tag.tag); } function switchToTag(outputName, tagIndex) { - const tagmask = 1 << tagIndex - setTags(outputName, tagmask, 0) + const tagmask = 1 << tagIndex; + setTags(outputName, tagmask, 0); } function toggleTag(outputName, tagIndex) { - const output = getOutputState(outputName) + const output = getOutputState(outputName); if (!output || !output.tags) { - console.log("toggleTag: no output or tags for", outputName) - return + console.log("toggleTag: no output or tags for", outputName); + return; } - let currentMask = 0 + let currentMask = 0; output.tags.forEach(tag => { if (tag.state === 1) { - currentMask |= (1 << tag.tag) + currentMask |= (1 << tag.tag); } - }) + }); - const clickedMask = 1 << tagIndex - const newMask = currentMask ^ clickedMask + const clickedMask = 1 << tagIndex; + const newMask = currentMask ^ clickedMask; - console.log("toggleTag:", outputName, "tag:", tagIndex, "currentMask:", currentMask.toString(2), "clickedMask:", clickedMask.toString(2), "newMask:", newMask.toString(2)) + console.log("toggleTag:", outputName, "tag:", tagIndex, "currentMask:", currentMask.toString(2), "clickedMask:", clickedMask.toString(2), "newMask:", newMask.toString(2)); if (newMask === 0) { - console.log("toggleTag: newMask is 0, switching to tag", tagIndex) - setTags(outputName, 1 << tagIndex, 0) + console.log("toggleTag: newMask is 0, switching to tag", tagIndex); + setTags(outputName, 1 << tagIndex, 0); } else { - console.log("toggleTag: setting combined mask", newMask) - setTags(outputName, newMask, 0) + console.log("toggleTag: setting combined mask", newMask); + setTags(outputName, newMask, 0); } } function quit() { - Quickshell.execDetached(["mmsg", "-d", "quit"]) + Quickshell.execDetached(["mmsg", "-d", "quit"]); } Process { @@ -210,55 +211,56 @@ Singleton { stdout: StdioCollector { onStreamFinished: { try { - const newScales = {} - const lines = text.trim().split('\n') + const newScales = {}; + const lines = text.trim().split('\n'); for (const line of lines) { - const parts = line.trim().split(/\s+/) + const parts = line.trim().split(/\s+/); if (parts.length >= 3 && parts[1] === "scale_factor") { - const outputName = parts[0] - const scale = parseFloat(parts[2]) + const outputName = parts[0]; + const scale = parseFloat(parts[2]); if (!isNaN(scale)) { - newScales[outputName] = scale + newScales[outputName] = scale; } } } - outputScales = newScales + outputScales = newScales; } catch (e) { - console.warn("DwlService: Failed to parse mmsg output:", e) + console.warn("DwlService: Failed to parse mmsg output:", e); } } } onExited: exitCode => { if (exitCode !== 0) { - console.warn("DwlService: mmsg failed with exit code:", exitCode) + console.warn("DwlService: mmsg failed with exit code:", exitCode); } } } function refreshOutputScales() { - if (!dwlAvailable) return - scaleQueryProcess.running = true + if (!dwlAvailable) + return; + scaleQueryProcess.running = true; } function getOutputScale(outputName) { - return outputScales[outputName] + return outputScales[outputName]; } function getVisibleTags(outputName) { - const output = getOutputState(outputName) + const output = getOutputState(outputName); if (!output || !output.tags) { - return [] + return []; } - const visibleTags = new Set() + const visibleTags = new Set(); output.tags.forEach(tag => { if (tag.state === 1 || tag.clients > 0) { - visibleTags.add(tag.tag) + visibleTags.add(tag.tag); } - }) + }); - return Array.from(visibleTags).sort((a, b) => a - b) + return Array.from(visibleTags).sort((a, b) => a - b); } } diff --git a/quickshell/Services/ExtWorkspaceService.qml b/quickshell/Services/ExtWorkspaceService.qml index dd3602ed..e6784827 100644 --- a/quickshell/Services/ExtWorkspaceService.qml +++ b/quickshell/Services/ExtWorkspaceService.qml @@ -47,7 +47,7 @@ Singleton { 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) + const useExtWorkspace = DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll) if (!useExtWorkspace) { console.info("ExtWorkspaceService: ext-workspace available but compositor has native support") extWorkspaceAvailable = false diff --git a/quickshell/Services/SessionService.qml b/quickshell/Services/SessionService.qml index cfbf1bc6..d477327c 100644 --- a/quickshell/Services/SessionService.qml +++ b/quickshell/Services/SessionService.qml @@ -110,9 +110,9 @@ Singleton { onExited: function (exitCode) { if (exitCode === 0) { - nvidiaCommand = "prime-run" + nvidiaCommand = "prime-run"; } else { - detectNvidiaOffloadProcess.running = true + detectNvidiaOffloadProcess.running = true; } } } @@ -124,7 +124,7 @@ Singleton { onExited: function (exitCode) { if (exitCode === 0) { - nvidiaCommand = "nvidia-offload" + nvidiaCommand = "nvidia-offload"; } } } @@ -243,7 +243,7 @@ Singleton { return; } - if (CompositorService.isSway) { + if (CompositorService.isSway || CompositorService.isScroll) { try { I3.dispatch("exit"); } catch (_) {}