1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-12 00:32:17 -04:00

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 <bbedward@gmail.com>
This commit is contained in:
Varshit
2025-12-09 21:57:46 +01:00
committed by GitHub
parent aeacf109eb
commit f94011cf05
18 changed files with 298 additions and 169 deletions

View File

@@ -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 || "";
}

View File

@@ -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 (_) {}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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 (_) {}