diff --git a/Modules/DankBar/Widgets/LauncherButton.qml b/Modules/DankBar/Widgets/LauncherButton.qml index 1880f7e0..6b147005 100644 --- a/Modules/DankBar/Widgets/LauncherButton.qml +++ b/Modules/DankBar/Widgets/LauncherButton.qml @@ -56,7 +56,7 @@ BasePill { } IconImage { - visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway) + visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isLabwc) anchors.centerIn: parent width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset) height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset) @@ -71,6 +71,8 @@ BasePill { return "file://" + Theme.shellDir + "/assets/mango.png" } else if (CompositorService.isSway) { return "file://" + Theme.shellDir + "/assets/sway.svg" + } else if (CompositorService.isLabwc) { + return "file://" + Theme.shellDir + "/assets/labwc.png" } return "" } diff --git a/Modules/Settings/AboutTab.qml b/Modules/Settings/AboutTab.qml index 25c61c6c..971d2327 100644 --- a/Modules/Settings/AboutTab.qml +++ b/Modules/Settings/AboutTab.qml @@ -13,11 +13,13 @@ Item { property bool isNiri: CompositorService.isNiri property bool isSway: CompositorService.isSway property bool isDwl: CompositorService.isDwl + property bool isLabwc: CompositorService.isLabwc property string compositorName: { if (isHyprland) return "hyprland" if (isSway) return "sway" if (isDwl) return "mangowc" + if (isLabwc) return "labwc" return "niri" } @@ -25,6 +27,7 @@ Item { if (isHyprland) return "/assets/hyprland.svg" if (isSway) return "/assets/sway.svg" if (isDwl) return "/assets/mango.png" + if (isLabwc) return "/assets/labwc.png" return "/assets/niri.svg" } @@ -32,6 +35,7 @@ Item { if (isHyprland) return "https://hypr.land" if (isSway) return "https://swaywm.org" if (isDwl) return "https://github.com/DreamMaoMao/mangowc" + if (isLabwc) return "https://labwc.github.io/" return "https://github.com/YaLTeR/niri" } @@ -39,6 +43,7 @@ Item { if (isHyprland) return "Hyprland Website" if (isSway) return "Sway Website" if (isDwl) return "mangowc GitHub" + if (isLabwc) return "LabWC Website" return "niri GitHub" } @@ -60,9 +65,13 @@ Item { property string redditUrl: "https://reddit.com/r/niri" property string redditTooltip: "r/niri Subreddit" - property bool showMatrix: isNiri && !isHyprland && !isSway && !isDwl + 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 showCompositorDiscord: isHyprland || isDwl - property bool showReddit: isNiri && !isHyprland && !isSway && !isDwl + property bool showReddit: isNiri && !isHyprland && !isSway && !isDwl && !isLabwc + property bool showIrc: isLabwc DankFlickable { anchors.fill: parent @@ -153,6 +162,9 @@ Item { if (showMatrix) { baseWidth += matrixButton.width + 4 } + if (showIrc) { + baseWidth += ircButton.width + Theme.spacingM + } if (showCompositorDiscord) { baseWidth += compositorDiscordButton.width + Theme.spacingM } @@ -232,11 +244,43 @@ Item { } } + Item { + id: ircButton + width: 24 + height: 24 + x: compositorButton.x + compositorButton.width + Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + visible: showIrc + + property bool hovered: false + property string tooltipText: ircTooltip + + DankIcon { + anchors.centerIn: parent + name: "forum" + size: 20 + color: Theme.surfaceText + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.hovered = true + onExited: parent.hovered = false + onClicked: Qt.openUrlExternally(ircUrl) + } + } + Item { id: dmsDiscordButton width: 20 height: 20 - x: showMatrix ? matrixButton.x + matrixButton.width + Theme.spacingM : compositorButton.x + compositorButton.width + Theme.spacingM + x: { + if (showMatrix) return matrixButton.x + matrixButton.width + Theme.spacingM + if (showIrc) return ircButton.x + ircButton.width + Theme.spacingM + return compositorButton.x + compositorButton.width + Theme.spacingM + } anchors.verticalCenter: parent.verticalCenter property bool hovered: false @@ -618,6 +662,7 @@ Item { property var hoveredButton: { if (compositorButton.hovered) return compositorButton if (matrixButton.visible && matrixButton.hovered) return matrixButton + if (ircButton.visible && ircButton.hovered) return ircButton if (dmsDiscordButton.hovered) return dmsDiscordButton if (compositorDiscordButton.visible && compositorDiscordButton.hovered) return compositorDiscordButton if (redditButton.visible && redditButton.hovered) return redditButton diff --git a/README.md b/README.md index bb2253d8..767c8962 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ -DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hypr.land), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop - all in one cohesive package with a gorgeous interface. +DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hypr.land), [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 - all in one cohesive package with a gorgeous interface. ## Components @@ -100,7 +100,7 @@ Endless customization with the [plugin registry](https://plugins.danklinux.com). ## Supported Compositors -DankMaterialShell works best with **[niri](https://github.com/YaLTeR/niri)**, **[Hyprland](https://hyprland.org/)**, **[sway](https://swaywm.org/)**, and **[dwl/MangoWC](https://github.com/DreamMaoMao/mangowc)** - with full workspace switching, overview integration, and monitor management. +DankMaterialShell works best with **[niri](https://github.com/YaLTeR/niri)**, **[Hyprland](https://hyprland.org/)**, **[sway](https://swaywm.org/)**, **[dwl/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 too, just with a reduced feature set. diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index 90b8eb99..a3d1bf85 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -14,11 +14,13 @@ Singleton { property bool isNiri: false property bool isDwl: false property bool isSway: false + property bool isLabwc: 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") + readonly property string labwcPid: Quickshell.env("LABWC_PID") property bool useNiriSorting: isNiri && NiriService property var sortedToplevels: [] @@ -33,6 +35,13 @@ Singleton { return screen.devicePixelRatio || 1 } + if (WlrOutputService.wlrOutputAvailable && screen) { + const wlrOutput = WlrOutputService.getOutput(screen.name) + if (wlrOutput?.enabled && wlrOutput.scale !== undefined && wlrOutput.scale > 0) { + return wlrOutput.scale + } + } + if (isNiri && screen) { const niriScale = NiriService.displayScales[screen.name] if (niriScale !== undefined) return niriScale @@ -343,6 +352,7 @@ Singleton { isNiri = false isDwl = false isSway = false + isLabwc = false compositor = "hyprland" console.info("CompositorService: Detected Hyprland") return @@ -355,6 +365,7 @@ Singleton { isHyprland = false isDwl = false isSway = false + isLabwc = false compositor = "niri" console.info("CompositorService: Detected Niri with socket:", niriSocket) NiriService.generateNiriBinds() @@ -371,13 +382,25 @@ Singleton { isHyprland = false isDwl = false isSway = true + isLabwc = false compositor = "sway" console.info("CompositorService: Detected Sway with socket:", swaySocket) } }, 0) - return + return } - + + if (labwcPid && labwcPid.length > 0) { + isHyprland = false + isNiri = false + isDwl = false + isSway = false + isLabwc = true + compositor = "labwc" + console.info("CompositorService: Detected LabWC with PID:", labwcPid) + return + } + if (DMSService.dmsAvailable) { Qt.callLater(checkForDwl) } else { @@ -385,6 +408,7 @@ Singleton { isNiri = false isDwl = false isSway = false + isLabwc = false compositor = "unknown" console.warn("CompositorService: No compositor detected") } @@ -393,7 +417,7 @@ Singleton { Connections { target: DMSService function onCapabilitiesReceived() { - if (!isHyprland && !isNiri && !isDwl) { + if (!isHyprland && !isNiri && !isDwl && !isLabwc) { checkForDwl() } } @@ -404,6 +428,8 @@ Singleton { isHyprland = false isNiri = false isDwl = true + isSway = false + isLabwc = false compositor = "dwl" console.info("CompositorService: Detected DWL via DMS capability") } diff --git a/Services/CupsService.qml b/Services/CupsService.qml index 83db99df..5e358d3c 100644 --- a/Services/CupsService.qml +++ b/Services/CupsService.qml @@ -1,6 +1,6 @@ pragma Singleton -pragma ComponentBehavior +pragma ComponentBehavior: Bound import QtQuick import Quickshell diff --git a/Services/DMSService.qml b/Services/DMSService.qml index 785ea311..6c74377d 100644 --- a/Services/DMSService.qml +++ b/Services/DMSService.qml @@ -48,8 +48,9 @@ Singleton { signal brightnessStateUpdate(var data) signal brightnessDeviceUpdate(var device) signal extWorkspaceStateUpdate(var data) + signal wlrOutputStateUpdate(var data) - property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness"] + property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput"] Component.onCompleted: { if (socketPath && socketPath.length > 0) { @@ -346,6 +347,8 @@ Singleton { } } else if (service === "extworkspace") { extWorkspaceStateUpdate(data) + } else if (service === "wlroutput") { + wlrOutputStateUpdate(data) } } diff --git a/Services/WlrOutputService.qml b/Services/WlrOutputService.qml new file mode 100644 index 00000000..2810e629 --- /dev/null +++ b/Services/WlrOutputService.qml @@ -0,0 +1,275 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell + +Singleton { + id: root + + property bool wlrOutputAvailable: false + property var outputs: [] + property int serial: 0 + + signal stateChanged + signal configurationApplied(bool success, string message) + + Connections { + target: DMSService + + function onCapabilitiesReceived() { + checkCapabilities() + } + + function onConnectionStateChanged() { + if (DMSService.isConnected) { + checkCapabilities() + return + } + wlrOutputAvailable = false + } + + function onWlrOutputStateUpdate(data) { + if (!wlrOutputAvailable) { + return + } + handleStateUpdate(data) + } + } + + Component.onCompleted: { + if (!DMSService.dmsAvailable) { + return + } + checkCapabilities() + } + + function checkCapabilities() { + if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) { + wlrOutputAvailable = false + return + } + + const hasWlrOutput = DMSService.capabilities.includes("wlroutput") + if (hasWlrOutput && !wlrOutputAvailable) { + wlrOutputAvailable = true + console.info("WlrOutputService: wlr-output-management capability detected") + requestState() + return + } + + if (!hasWlrOutput) { + wlrOutputAvailable = false + } + } + + function requestState() { + if (!DMSService.isConnected || !wlrOutputAvailable) { + return + } + + DMSService.sendRequest("wlroutput.getState", null, response => { + if (!response.result) { + return + } + handleStateUpdate(response.result) + }) + } + + function handleStateUpdate(state) { + outputs = state.outputs || [] + serial = state.serial || 0 + + if (outputs.length === 0) { + console.warn("WlrOutputService: Received empty outputs list") + } else { + console.log("WlrOutputService: Updated with", outputs.length, "outputs, serial:", serial) + outputs.forEach((output, index) => { + console.log("WlrOutputService: Output", index, "-", output.name, + "enabled:", output.enabled, + "mode:", output.currentMode ? + output.currentMode.width + "x" + output.currentMode.height + "@" + + (output.currentMode.refresh / 1000) + "Hz" : "none") + }) + } + stateChanged() + } + + function getOutput(name) { + for (const output of outputs) { + if (output.name === name) { + return output + } + } + return null + } + + function getEnabledOutputs() { + return outputs.filter(output => output.enabled) + } + + function applyConfiguration(heads, callback) { + if (!DMSService.isConnected || !wlrOutputAvailable) { + if (callback) { + callback(false, "Not connected") + } + return + } + + console.log("WlrOutputService: Applying configuration for", heads.length, "outputs") + heads.forEach((head, index) => { + console.log("WlrOutputService: Head", index, "- name:", head.name, + "enabled:", head.enabled, + "modeId:", head.modeId, + "customMode:", JSON.stringify(head.customMode), + "position:", JSON.stringify(head.position), + "scale:", head.scale, + "transform:", head.transform, + "adaptiveSync:", head.adaptiveSync) + }) + + DMSService.sendRequest("wlroutput.applyConfiguration", { + "heads": heads + }, response => { + const success = !response.error + const message = response.error || response.result?.message || "" + + if (response.error) { + console.warn("WlrOutputService: applyConfiguration error:", response.error) + } else { + console.log("WlrOutputService: Configuration applied successfully") + } + + configurationApplied(success, message) + if (callback) { + callback(success, message) + } + }) + } + + function testConfiguration(heads, callback) { + if (!DMSService.isConnected || !wlrOutputAvailable) { + if (callback) { + callback(false, "Not connected") + } + return + } + + console.log("WlrOutputService: Testing configuration for", heads.length, "outputs") + + DMSService.sendRequest("wlroutput.testConfiguration", { + "heads": heads + }, response => { + const success = !response.error + const message = response.error || response.result?.message || "" + + if (response.error) { + console.warn("WlrOutputService: testConfiguration error:", response.error) + } else { + console.log("WlrOutputService: Configuration test passed") + } + + if (callback) { + callback(success, message) + } + }) + } + + function setOutputEnabled(outputName, enabled, callback) { + const output = getOutput(outputName) + if (!output) { + console.warn("WlrOutputService: Output not found:", outputName) + if (callback) { + callback(false, "Output not found") + } + return + } + + const heads = [{ + "name": outputName, + "enabled": enabled + }] + + if (enabled && output.currentMode) { + heads[0].modeId = output.currentMode.id + } + + applyConfiguration(heads, callback) + } + + function setOutputMode(outputName, modeId, callback) { + const heads = [{ + "name": outputName, + "enabled": true, + "modeId": modeId + }] + + applyConfiguration(heads, callback) + } + + function setOutputCustomMode(outputName, width, height, refresh, callback) { + const heads = [{ + "name": outputName, + "enabled": true, + "customMode": { + "width": width, + "height": height, + "refresh": refresh + } + }] + + applyConfiguration(heads, callback) + } + + function setOutputPosition(outputName, x, y, callback) { + const heads = [{ + "name": outputName, + "enabled": true, + "position": { + "x": x, + "y": y + } + }] + + applyConfiguration(heads, callback) + } + + function setOutputScale(outputName, scale, callback) { + const heads = [{ + "name": outputName, + "enabled": true, + "scale": scale + }] + + applyConfiguration(heads, callback) + } + + function setOutputTransform(outputName, transform, callback) { + const heads = [{ + "name": outputName, + "enabled": true, + "transform": transform + }] + + applyConfiguration(heads, callback) + } + + function setOutputAdaptiveSync(outputName, state, callback) { + const heads = [{ + "name": outputName, + "enabled": true, + "adaptiveSync": state + }] + + applyConfiguration(heads, callback) + } + + function configureOutput(config, callback) { + const heads = [config] + applyConfiguration(heads, callback) + } + + function configureMultipleOutputs(configs, callback) { + applyConfiguration(configs, callback) + } +} diff --git a/assets/labwc.png b/assets/labwc.png new file mode 100644 index 00000000..e60bec3b Binary files /dev/null and b/assets/labwc.png differ diff --git a/translations/en.json b/translations/en.json index d9985033..417c93df 100644 --- a/translations/en.json +++ b/translations/en.json @@ -14,13 +14,13 @@ { "term": "- Stateless System Monitoring", "context": "- Stateless System Monitoring", - "reference": "Modules/Settings/AboutTab.qml:544", + "reference": "Modules/Settings/AboutTab.qml:588", "comment": "" }, { "term": "- Support Us With a Star ⭐", "context": "- Support Us With a Star ⭐", - "reference": "Modules/Settings/AboutTab.qml:509", + "reference": "Modules/Settings/AboutTab.qml:553", "comment": "" }, { @@ -62,7 +62,7 @@ { "term": "About", "context": "About", - "reference": "Modals/Settings/SettingsSidebar.qml:44, Modules/Settings/AboutTab.qml:363", + "reference": "Modals/Settings/SettingsSidebar.qml:44, Modules/Settings/AboutTab.qml:407", "comment": "" }, { @@ -1010,7 +1010,7 @@ { "term": "DMS out of date", "context": "DMS out of date", - "reference": "Services/DMSService.qml:300", + "reference": "Services/DMSService.qml:301", "comment": "" }, { @@ -1262,7 +1262,7 @@ { "term": "Donate on Ko-fi", "context": "Donate on Ko-fi", - "reference": "Modules/Settings/AboutTab.qml:598", + "reference": "Modules/Settings/AboutTab.qml:642", "comment": "" }, { @@ -1688,7 +1688,7 @@ { "term": "Github:", "context": "Github:", - "reference": "Modules/Settings/AboutTab.qml:482", + "reference": "Modules/Settings/AboutTab.qml:526", "comment": "" }, { @@ -2798,7 +2798,7 @@ { "term": "Plugins:", "context": "Plugins:", - "reference": "Modules/Settings/AboutTab.qml:459", + "reference": "Modules/Settings/AboutTab.qml:503", "comment": "" }, { @@ -3026,7 +3026,7 @@ { "term": "Resources", "context": "Resources", - "reference": "Modules/Settings/AboutTab.qml:421", + "reference": "Modules/Settings/AboutTab.qml:465", "comment": "" }, { @@ -3500,7 +3500,7 @@ { "term": "Support Development", "context": "Support Development", - "reference": "Modules/Settings/AboutTab.qml:583", + "reference": "Modules/Settings/AboutTab.qml:627", "comment": "" }, { @@ -3578,7 +3578,7 @@ { "term": "System Monitoring:", "context": "System Monitoring:", - "reference": "Modules/Settings/AboutTab.qml:517", + "reference": "Modules/Settings/AboutTab.qml:561", "comment": "" }, { @@ -3728,7 +3728,7 @@ { "term": "To update, run the following command:", "context": "To update, run the following command:", - "reference": "Services/DMSService.qml:300", + "reference": "Services/DMSService.qml:301", "comment": "" }, { @@ -4052,7 +4052,7 @@ { "term": "Website:", "context": "Website:", - "reference": "Modules/Settings/AboutTab.qml:436", + "reference": "Modules/Settings/AboutTab.qml:480", "comment": "" }, {