1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-13 06:33:30 -04:00

feat(mango): first-class MangoWM support across DMS, dankinstaller & UI tools

- Bring up Mango to parity with niri/hyprland via a native JSON-IPC w/Native MangoServic., replaces the legacy dwl/`mmsg` path and recent breaking changes
- Dankinstall: mango supported installer, config/binds templates, and packaging (Arch AUR, Fedora Terra auto-enable, Gentoo GURU)
- Window rules: Go provider + CLI + Settings GUI editor
- Keybinds + config reload on edit (mmsg dispatch reload_config)
- Misc new supported options in DMS settings
This commit is contained in:
purian23
2026-06-04 18:45:04 -04:00
parent 4181343ef3
commit 8eb23bcc29
63 changed files with 2282 additions and 301 deletions
+10 -1
View File
@@ -490,6 +490,9 @@ Singleton {
},
"dwl": {
"cursorHideTimeout": 0
},
"mango": {
"cursorHideTimeout": 0
}
})
property var availableCursorThemes: ["System Default"]
@@ -1222,6 +1225,8 @@ Singleton {
HyprlandService.generateLayoutConfig();
if (CompositorService.isDwl && typeof DwlService !== "undefined")
DwlService.generateLayoutConfig();
if (CompositorService.isMango && typeof MangoService !== "undefined")
MangoService.generateLayoutConfig();
}
function applyStoredIconTheme() {
@@ -2235,7 +2240,7 @@ Singleton {
function getFilteredScreens(componentId) {
var prefs = screenPreferences && screenPreferences[componentId] || ["all"];
if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) {
if (!prefs || prefs.length === 0 || prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) {
return Quickshell.screens;
}
var filtered = Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
@@ -2446,6 +2451,10 @@ Singleton {
DwlService.generateCursorConfig();
return;
}
if (CompositorService.isMango && typeof MangoService !== "undefined") {
MangoService.generateCursorConfig();
return;
}
}
function updateXResources() {
+3
View File
@@ -340,6 +340,9 @@ Item {
if (CompositorService.isDwl && DwlService.activeOutput) {
return DwlService.activeOutput;
}
if (CompositorService.isMango && MangoService.activeOutput) {
return MangoService.activeOutput;
}
return "";
}
@@ -322,6 +322,8 @@ Item {
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-1";
else if (CompositorService.isDwl)
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2";
else if (CompositorService.isMango)
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2";
Qt.openUrlExternally(url);
}
}
@@ -130,7 +130,7 @@ Item {
title: I18n.tr("Multi-Monitor", "greeter feature card title")
description: I18n.tr("Per-screen config", "greeter feature card description")
onClicked: {
const hasDisplayConfig = CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl;
const hasDisplayConfig = CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango;
PopoutService.openSettingsWithTab(hasDisplayConfig ? "display_config" : "display_widgets");
}
}
@@ -311,7 +311,7 @@ Rectangle {
"text": I18n.tr("Window Rules"),
"icon": "select_window",
"tabIndex": 28,
"hyprlandNiriOnly": true
"windowRulesCapable": true
}
]
},
@@ -370,6 +370,8 @@ Rectangle {
return false;
if (item.hyprlandNiriOnly && !CompositorService.isNiri && !CompositorService.isHyprland)
return false;
if (item.windowRulesCapable && !CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
return false;
if (item.niriOnly && !CompositorService.isNiri)
return false;
if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23))
+183 -5
View File
@@ -12,6 +12,7 @@ FloatingWindow {
property bool isEditMode: editingRule !== null
property bool isNiri: CompositorService.isNiri
property bool isHyprland: CompositorService.isHyprland
property bool isMango: CompositorService.isMango
property bool submitting: false
property var targetWindow: null
@@ -95,6 +96,14 @@ FloatingWindow {
moveInput.text = "";
monitorInput.text = "";
hyprWorkspaceInput.text = "";
mangoTagsInput.text = "";
mangoMonitorInput.text = "";
mangoSizeInput.text = "";
mangoNoBlurToggle.checked = false;
mangoNoBorderToggle.checked = false;
mangoNoShadowToggle.checked = false;
mangoNoRoundingToggle.checked = false;
mangoNoAnimToggle.checked = false;
}
function show(window) {
@@ -103,7 +112,10 @@ FloatingWindow {
resetForm();
if (targetWindow) {
nameInput.text = targetWindow.appId || "";
appIdInput.text = targetWindow.appId ? "^" + targetWindow.appId + "$" : "";
if (targetWindow.appId)
appIdInput.text = isMango ? targetWindow.appId : "^" + targetWindow.appId + "$";
else
appIdInput.text = "";
}
visible = true;
Qt.callLater(() => nameInput.forceActiveFocus());
@@ -209,6 +221,15 @@ FloatingWindow {
moveInput.text = actions.move || "";
monitorInput.text = actions.monitor || "";
hyprWorkspaceInput.text = actions.workspace || "";
mangoTagsInput.text = actions.workspace || "";
mangoMonitorInput.text = actions.monitor || "";
mangoSizeInput.text = actions.size || "";
mangoNoBlurToggle.checked = actions.noblur || false;
mangoNoBorderToggle.checked = actions.noborder || false;
mangoNoShadowToggle.checked = actions.noshadow || false;
mangoNoRoundingToggle.checked = actions.norounding || false;
mangoNoAnimToggle.checked = actions.noanim || false;
}
function showEdit(rule) {
@@ -387,6 +408,25 @@ FloatingWindow {
actions.workspace = hyprWorkspaceInput.text.trim();
}
if (isMango) {
if (mangoTagsInput.text.trim())
actions.workspace = mangoTagsInput.text.trim();
if (mangoMonitorInput.text.trim())
actions.monitor = mangoMonitorInput.text.trim();
if (mangoSizeInput.text.trim())
actions.size = mangoSizeInput.text.trim();
if (mangoNoBlurToggle.checked)
actions.noblur = true;
if (mangoNoBorderToggle.checked)
actions.noborder = true;
if (mangoNoShadowToggle.checked)
actions.noshadow = true;
if (mangoNoRoundingToggle.checked)
actions.norounding = true;
if (mangoNoAnimToggle.checked)
actions.noanim = true;
}
const name = nameInput.text.trim() || matchCriteria.appId || I18n.tr("Rule");
const compositor = CompositorService.compositor;
@@ -411,6 +451,8 @@ FloatingWindow {
return;
if (shouldValidate)
NiriService.validate();
if (CompositorService.isMango)
MangoService.reloadConfig();
root.ruleSubmitted();
root.hide();
});
@@ -422,6 +464,8 @@ FloatingWindow {
return;
if (shouldValidate)
NiriService.validate();
if (CompositorService.isMango)
MangoService.reloadConfig();
root.ruleSubmitted();
root.hide();
});
@@ -664,7 +708,7 @@ FloatingWindow {
anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText
placeholderText: isNiri ? I18n.tr("App ID regex (e.g. ^firefox$)") : I18n.tr("Class regex (e.g. ^firefox$)")
placeholderText: isMango ? I18n.tr("App ID (e.g. firefox)") : isHyprland ? I18n.tr("Class regex (e.g. ^firefox$)") : I18n.tr("App ID regex (e.g. ^firefox$)")
backgroundColor: "transparent"
enabled: root.visible
}
@@ -682,7 +726,7 @@ FloatingWindow {
anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText
placeholderText: I18n.tr("Title regex (optional)")
placeholderText: isMango ? I18n.tr("Title (optional)") : I18n.tr("Title regex (optional)")
backgroundColor: "transparent"
enabled: root.visible
}
@@ -702,7 +746,7 @@ FloatingWindow {
onClicked: {
if (!root.targetWindow?.title)
return;
titleInput.text = "^" + root.targetWindow.title + "$";
titleInput.text = isMango ? root.targetWindow.title : "^" + root.targetWindow.title + "$";
}
}
}
@@ -807,10 +851,12 @@ FloatingWindow {
SectionHeader {
title: I18n.tr("Match Conditions")
visible: isNiri || isHyprland
}
StyledText {
width: parent.width
visible: isNiri || isHyprland
text: I18n.tr("Optional state-based conditions applied to the first match.")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
@@ -820,6 +866,7 @@ FloatingWindow {
Flow {
width: parent.width
spacing: Theme.spacingS
visible: isNiri || isHyprland
MatchCond {
id: condFloating
@@ -892,6 +939,7 @@ FloatingWindow {
CheckboxRow {
id: maximizedToggle
label: I18n.tr("Maximize")
visible: !isMango
}
CheckboxRow {
id: fullscreenToggle
@@ -912,7 +960,7 @@ FloatingWindow {
Row {
width: parent.width
spacing: Theme.spacingM
visible: true
visible: isNiri || isHyprland
Column {
width: (parent.width - Theme.spacingM) / 2
@@ -1031,11 +1079,13 @@ FloatingWindow {
SectionHeader {
title: I18n.tr("Dynamic Properties")
visible: isNiri || isHyprland
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: isNiri || isHyprland
CheckboxRow {
id: opacityEnabled
@@ -1154,6 +1204,7 @@ FloatingWindow {
Row {
width: parent.width
spacing: Theme.spacingM
visible: isNiri || isHyprland
CheckboxRow {
id: cornerRadiusEnabled
@@ -1352,11 +1403,13 @@ FloatingWindow {
SectionHeader {
title: I18n.tr("Size Constraints")
visible: isNiri || isHyprland
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: isNiri || isHyprland
Column {
width: (parent.width - Theme.spacingM * 3) / 4
@@ -1639,6 +1692,131 @@ FloatingWindow {
}
}
SectionHeader {
title: I18n.tr("Mango Options")
visible: isMango
}
Flow {
width: parent.width
spacing: Theme.spacingL
visible: isMango
CheckboxRow {
id: mangoNoBlurToggle
label: I18n.tr("No Blur")
}
CheckboxRow {
id: mangoNoBorderToggle
label: I18n.tr("No Border")
}
CheckboxRow {
id: mangoNoShadowToggle
label: I18n.tr("No Shadow")
}
CheckboxRow {
id: mangoNoRoundingToggle
label: I18n.tr("No Rounding")
}
CheckboxRow {
id: mangoNoAnimToggle
label: I18n.tr("No Anim")
}
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: isMango
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Tags")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
width: parent.width
hasFocus: mangoTagsInput.activeFocus
DankTextField {
id: mangoTagsInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText
placeholderText: "1"
backgroundColor: "transparent"
enabled: root.visible
}
}
}
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Monitor")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
width: parent.width
hasFocus: mangoMonitorInput.activeFocus
DankTextField {
id: mangoMonitorInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText
placeholderText: "HDMI-A-1"
backgroundColor: "transparent"
enabled: root.visible
}
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: isMango
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Size")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
width: parent.width
hasFocus: mangoSizeInput.activeFocus
DankTextField {
id: mangoSizeInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText
placeholderText: "800x600"
backgroundColor: "transparent"
enabled: root.visible
}
}
}
}
Item {
width: 1
height: Theme.spacingM
+4
View File
@@ -110,6 +110,8 @@ Item {
focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {
focusedScreenName = DwlService.activeOutput;
} else if (CompositorService.isMango && MangoService.activeOutput) {
focusedScreenName = MangoService.activeOutput;
}
if (!focusedScreenName && barVariants.instances.length > 0) {
@@ -139,6 +141,8 @@ Item {
focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {
focusedScreenName = DwlService.activeOutput;
} else if (CompositorService.isMango && MangoService.activeOutput) {
focusedScreenName = MangoService.activeOutput;
}
if (!focusedScreenName && barVariants.instances.length > 0) {
+11 -10
View File
@@ -29,6 +29,7 @@ Item {
readonly property real _frameEdgeFloorInset: (SettingsData.frameEnabled && _usesFrameBarChrome) ? Math.max(0, SettingsData.frameThickness - _edgeBaseMargin) : 0
readonly property bool _barIsVertical: _hasBarWindow ? barWindow.isVertical : false
readonly property string _barScreenName: _hasBarWindow ? (barWindow.screenName || "") : ""
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
readonly property bool hasAdjacentTopBarLive: _hasBarWindow && barWindow.hasAdjacentTopBar
readonly property bool hasAdjacentBottomBarLive: _hasBarWindow && barWindow.hasAdjacentBottomBar
readonly property bool hasAdjacentLeftBarLive: _hasBarWindow && barWindow.hasAdjacentLeftBar
@@ -189,16 +190,16 @@ Item {
}
return monitorWorkspaces.sort((a, b) => a.id - b.id);
} else if (CompositorService.isDwl) {
if (!DwlService.dwlAvailable) {
} else if (CompositorService.isDwl || CompositorService.isMango) {
if (!dwlSvc.available) {
return [0];
}
if (SettingsData.dwlShowAllTags) {
return Array.from({
length: DwlService.tagCount
length: dwlSvc.tagCount
}, (_, i) => i);
}
return DwlService.getVisibleTags(screenName);
return dwlSvc.getVisibleTags(screenName);
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const workspaces = I3.workspaces?.values || [];
if (workspaces.length === 0)
@@ -234,13 +235,13 @@ Item {
const monitors = Hyprland.monitors?.values || [];
const currentMonitor = monitors.find(monitor => monitor.name === screenName);
return currentMonitor?.activeWorkspace?.id ?? 1;
} else if (CompositorService.isDwl) {
if (!DwlService.dwlAvailable)
} else if (CompositorService.isDwl || CompositorService.isMango) {
if (!dwlSvc.available)
return 0;
const outputState = DwlService.getOutputState(screenName);
const outputState = dwlSvc.getOutputState(screenName);
if (!outputState || !outputState.tags)
return 0;
const activeTags = DwlService.getActiveTags(screenName);
const activeTags = dwlSvc.getActiveTags(screenName);
return activeTags.length > 0 ? activeTags[0] : 0;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
if (!screenName || SettingsData.workspaceFollowFocus) {
@@ -282,14 +283,14 @@ Item {
if (nextIndex !== validIndex) {
HyprlandService.focusWorkspace(realWorkspaces[nextIndex].id);
}
} else if (CompositorService.isDwl) {
} else if (CompositorService.isDwl || CompositorService.isMango) {
const currentTag = getCurrentWorkspace();
const currentIndex = realWorkspaces.findIndex(tag => tag === currentTag);
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) {
DwlService.switchToTag(_barScreenName, realWorkspaces[nextIndex]);
dwlSvc.switchToTag(_barScreenName, realWorkspaces[nextIndex]);
}
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const currentWs = getCurrentWorkspace();
+20 -2
View File
@@ -327,6 +327,24 @@ PanelWindow {
hasMaximizedToplevel = false;
return;
}
if (CompositorService.isMango) {
const out = MangoService.outputs[screenName];
const active = new Set((out?.activeTags) || []);
const wins = MangoService.windows || [];
for (let i = 0; i < wins.length; i++) {
const w = wins[i];
if (!w || w.monitor !== screenName || w.is_minimized)
continue;
if (active.size > 0 && !(w.tags || []).some(t => active.has(t)))
continue;
if (w.is_maximized || w.is_fullscreen) {
hasMaximizedToplevel = true;
return;
}
}
hasMaximizedToplevel = false;
return;
}
if (!CompositorService.isHyprland && !CompositorService.isNiri) {
hasMaximizedToplevel = false;
return;
@@ -351,7 +369,7 @@ PanelWindow {
shouldHideForWindows = false;
return;
}
if (!CompositorService.isNiri && !CompositorService.isHyprland) {
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango) {
shouldHideForWindows = false;
return;
}
@@ -825,7 +843,7 @@ PanelWindow {
return true;
const showOnWindowsSetting = barConfig?.showOnWindowsOpen ?? false;
if (showOnWindowsSetting && autoHide && (CompositorService.isNiri || CompositorService.isHyprland)) {
if (showOnWindowsSetting && autoHide && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango)) {
if (barWindow.shouldHideForWindows)
return topBarMouseArea.containsMouse || popoutPinsReveal || revealSticky || ipcReveal;
return true;
@@ -10,6 +10,10 @@ DankPopout {
property var triggerScreen: null
// mango shares dwl's layout model; route to the right service.
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) {
triggerX = x;
triggerY = y;
@@ -33,8 +37,8 @@ DankPopout {
onScreenChanged: updateOutputState()
function updateOutputState() {
if (screen && DwlService.dwlAvailable) {
outputState = DwlService.getOutputState(screen.name);
if (screen && root.dwlSvc.available) {
outputState = root.dwlSvc.getOutputState(screen.name);
} else {
outputState = null;
}
@@ -215,7 +219,7 @@ DankPopout {
spacing: Theme.spacingS
Repeater {
model: DwlService.layouts
model: root.dwlSvc.layouts
delegate: Rectangle {
required property string modelData
@@ -269,11 +273,11 @@ DankPopout {
if (!root.triggerScreen) {
return;
}
if (!DwlService.dwlAvailable) {
if (!root.dwlSvc.available) {
return;
}
DwlService.setLayout(root.triggerScreen.name, index);
root.dwlSvc.setLayout(root.triggerScreen.name, index);
root.close();
}
}
+1 -1
View File
@@ -282,7 +282,7 @@ Loader {
"cpuTemp": dgopAvailable,
"gpuTemp": dgopAvailable,
"network_speed_monitor": dgopAvailable,
"layout": CompositorService.isDwl && DwlService.dwlAvailable
"layout": (CompositorService.isDwl && DwlService.dwlAvailable) || (CompositorService.isMango && MangoService.available)
};
return widgetVisibility[widgetId] ?? true;
@@ -12,9 +12,13 @@ BasePill {
signal toggleLayoutPopup
visible: CompositorService.isDwl && DwlService.dwlAvailable
// mango shares dwl's tag/layout model; route to the right service.
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
property var outputState: parentScreen ? DwlService.getOutputState(parentScreen.name) : null
visible: layout.isDwlLike && layout.dwlSvc.available
property var outputState: parentScreen ? layout.dwlSvc.getOutputState(parentScreen.name) : null
property string currentLayoutSymbol: outputState?.layoutSymbol || ""
property int currentLayoutIndex: outputState?.layout || 0
@@ -37,9 +41,9 @@ BasePill {
}
Connections {
target: DwlService
target: layout.dwlSvc
function onStateChanged() {
outputState = parentScreen ? DwlService.getOutputState(parentScreen.name) : null;
outputState = parentScreen ? layout.dwlSvc.getOutputState(parentScreen.name) : null;
}
}
@@ -97,13 +101,13 @@ BasePill {
}
onRightClicked: {
if (!parentScreen || !DwlService.dwlAvailable || DwlService.layouts.length === 0) {
if (!parentScreen || !layout.dwlSvc.available || layout.dwlSvc.layouts.length === 0) {
return;
}
const currentIndex = layout.currentLayoutIndex;
const nextIndex = (currentIndex + 1) % DwlService.layouts.length;
const nextIndex = (currentIndex + 1) % layout.dwlSvc.layouts.length;
DwlService.setLayout(parentScreen.name, nextIndex);
layout.dwlSvc.setLayout(parentScreen.name, nextIndex);
}
}
@@ -114,6 +114,8 @@ BasePill {
return NiriService.getCurrentKeyboardLayoutName();
} else if (CompositorService.isDwl) {
return DwlService.currentKeyboardLayout;
} else if (CompositorService.isMango) {
return MangoService.currentKeyboardLayout;
}
return "";
}
@@ -208,7 +210,9 @@ BasePill {
} else if (CompositorService.isHyprland) {
Quickshell.execDetached(["hyprctl", "switchxkblayout", root.hyprlandKeyboard, "next"]);
} else if (CompositorService.isDwl) {
Quickshell.execDetached(["mmsg", "-d", "switch_keyboard_layout"]);
Quickshell.execDetached(["mmsg", "dispatch", "switch_keyboard_layout"]);
} else if (CompositorService.isMango) {
MangoService.cycleKeyboardLayout();
}
}
}
@@ -55,7 +55,7 @@ BasePill {
}
IconImage {
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
@@ -68,6 +68,8 @@ BasePill {
return "file://" + Theme.shellDir + "/assets/hyprland.svg";
} else if (CompositorService.isDwl) {
return "file://" + Theme.shellDir + "/assets/mango.png";
} else if (CompositorService.isMango) {
return "file://" + Theme.shellDir + "/assets/mango.png";
} else if (CompositorService.isSway) {
return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isScroll) {
@@ -22,6 +22,11 @@ Item {
property var hyprlandOverviewLoader: null
property var parentScreen: null
// mango shares dwl's tag model; route to the right service so one set of
// branches serves both.
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
readonly property real _leftMargin: {
if (isVertical)
return 0;
@@ -76,7 +81,8 @@ Item {
case "hyprland":
return Hyprland.focusedWorkspace?.monitor?.name || root.screenName;
case "dwl":
return DwlService.activeOutput || root.screenName;
case "mango":
return root.dwlSvc.activeOutput || root.screenName;
case "sway":
case "scroll":
case "miracle":
@@ -95,6 +101,7 @@ Item {
case "niri":
case "hyprland":
case "dwl":
case "mango":
case "sway":
case "scroll":
case "miracle":
@@ -121,6 +128,7 @@ Item {
case "hyprland":
return getHyprlandActiveWorkspace();
case "dwl":
case "mango":
const activeTags = getDwlActiveTags();
return activeTags.length > 0 ? activeTags[0] : -1;
case "sway":
@@ -132,7 +140,7 @@ Item {
}
}
property var dwlActiveTags: {
if (CompositorService.isDwl) {
if (root.isDwlLike) {
return getDwlActiveTags();
}
return [];
@@ -152,6 +160,7 @@ Item {
baseList = getHyprlandWorkspaces();
break;
case "dwl":
case "mango":
baseList = getDwlTags();
break;
case "sway":
@@ -288,7 +297,7 @@ Item {
}
} else if (CompositorService.isHyprland) {
targetWorkspaceId = ws.id !== undefined ? ws.id : ws;
} else if (CompositorService.isDwl) {
} else if (root.isDwlLike) {
if (typeof ws !== "object" || ws.tag === undefined) {
return [];
}
@@ -308,8 +317,8 @@ Item {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false;
} else if (CompositorService.isDwl) {
const output = DwlService.getOutputState(root.effectiveScreenName);
} else if (root.isDwlLike) {
const output = root.dwlSvc.getOutputState(root.effectiveScreenName);
if (output && output.tags) {
const tag = output.tags.find(t => t.tag === targetWorkspaceId);
isActiveWs = tag ? (tag.state === 1) : false;
@@ -323,19 +332,25 @@ Item {
return;
}
let winWs = null;
if (CompositorService.isNiri) {
winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
winWs = w.workspace?.num;
if (CompositorService.isMango) {
// mangoTags are 1-based; targetWorkspaceId is 0-based.
if (!(w.mangoTags || []).includes(targetWorkspaceId + 1))
return;
} else {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w);
winWs = hyprToplevel?.workspace?.id;
}
let winWs = null;
if (CompositorService.isNiri) {
winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
winWs = w.workspace?.num;
} else {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w);
winWs = hyprToplevel?.workspace?.id;
}
if (winWs === undefined || winWs === null || winWs !== targetWorkspaceId) {
return;
if (winWs === undefined || winWs === null || winWs !== targetWorkspaceId) {
return;
}
}
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown");
@@ -391,7 +406,7 @@ Item {
"id": -1,
"name": ""
};
} else if (CompositorService.isDwl) {
} else if (root.isDwlLike) {
placeholder = {
"tag": -1
};
@@ -473,11 +488,11 @@ Item {
}
function getDwlTags() {
if (!DwlService.dwlAvailable)
if (!root.dwlSvc.available)
return [];
const targetScreen = root.effectiveScreenName;
const output = DwlService.getOutputState(targetScreen);
const output = root.dwlSvc.getOutputState(targetScreen);
if (!output || !output.tags || output.tags.length === 0)
return [];
@@ -490,7 +505,7 @@ Item {
}));
}
const visibleTagIndices = DwlService.getVisibleTags(targetScreen);
const visibleTagIndices = root.dwlSvc.getVisibleTags(targetScreen);
return visibleTagIndices.map(tagIndex => {
const tagData = output.tags.find(t => t.tag === tagIndex);
return {
@@ -503,10 +518,10 @@ Item {
}
function getDwlActiveTags() {
if (!DwlService.dwlAvailable)
if (!root.dwlSvc.available)
return [];
return DwlService.getActiveTags(root.effectiveScreenName);
return root.dwlSvc.getActiveTags(root.effectiveScreenName);
}
function getExtWorkspaceWorkspaces() {
@@ -557,7 +572,7 @@ Item {
return ws && ws.idx !== -1;
if (CompositorService.isHyprland)
return ws && ws.id !== -1;
if (CompositorService.isDwl)
if (root.isDwlLike)
return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return ws && ws.num !== -1;
@@ -586,8 +601,9 @@ Item {
}
break;
case "dwl":
case "mango":
if (data.tag !== undefined)
DwlService.switchToTag(root.screenName, data.tag);
root.dwlSvc.switchToTag(root.screenName, data.tag);
break;
case "sway":
case "scroll":
@@ -673,7 +689,7 @@ Item {
}
HyprlandService.focusWorkspace(realWorkspaces[nextIndex].id);
} else if (CompositorService.isDwl) {
} else if (root.isDwlLike) {
const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) {
return;
@@ -687,7 +703,7 @@ Item {
return;
}
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
root.dwlSvc.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) {
@@ -715,7 +731,7 @@ Item {
return (modelData?.idx !== undefined && modelData?.idx !== -1) ? modelData.idx : "";
if (CompositorService.isHyprland)
return modelData?.id || "";
if (CompositorService.isDwl)
if (root.isDwlLike)
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return modelData?.num || "";
@@ -730,7 +746,7 @@ Item {
isPlaceholder = modelData?.idx === -1;
} else if (CompositorService.isHyprland) {
isPlaceholder = modelData?.id === -1;
} else if (CompositorService.isDwl) {
} else if (root.isDwlLike) {
isPlaceholder = modelData?.tag === -1;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
isPlaceholder = modelData?.num === -1;
@@ -765,7 +781,7 @@ Item {
return getWorkspaceIndexFallback(modelData, index);
}
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || root.isDwlLike || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
@@ -983,7 +999,7 @@ Item {
return !!(modelData && modelData.idx === root.currentWorkspace);
if (CompositorService.isHyprland)
return !!(modelData && modelData.id === root.currentWorkspace);
if (CompositorService.isDwl)
if (root.isDwlLike)
return !!(modelData && root.dwlActiveTags.includes(modelData.tag));
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return !!(modelData && modelData.num === root.currentWorkspace);
@@ -992,7 +1008,7 @@ Item {
property bool isOccupied: {
if (CompositorService.isHyprland)
return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id);
if (CompositorService.isDwl)
if (root.isDwlLike)
return modelData.clients > 0;
if (CompositorService.isNiri) {
const workspace = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName);
@@ -1007,7 +1023,7 @@ Item {
return !!(modelData && modelData.idx === -1);
if (CompositorService.isHyprland)
return !!(modelData && modelData.id === -1);
if (CompositorService.isDwl)
if (root.isDwlLike)
return !!(modelData && modelData.tag === -1);
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return !!(modelData && modelData.num === -1);
@@ -1024,7 +1040,7 @@ Item {
return modelData?.urgent ?? false;
if (CompositorService.isNiri)
return loadedIsUrgent;
if (CompositorService.isDwl)
if (root.isDwlLike)
return modelData?.state === 2;
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return loadedIsUrgent;
@@ -1081,8 +1097,12 @@ Item {
winWs = hyprToplevel?.workspace?.id;
}
if (winWs !== targetWorkspaceId)
if (CompositorService.isMango) {
if (!(w.mangoTags || []).includes(targetWorkspaceId + 1))
continue;
} else if (winWs !== targetWorkspaceId) {
continue;
}
totalCount++;
const appKey = w.app_id || w.appId || w.class || w.windowClass || "unknown";
@@ -1311,8 +1331,8 @@ Item {
}
} else if (CompositorService.isHyprland && modelData?.id) {
HyprlandService.focusWorkspace(modelData.id);
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
DwlService.switchToTag(root.screenName, modelData.tag);
} else if (root.isDwlLike && modelData?.tag !== undefined) {
root.dwlSvc.switchToTag(root.screenName, modelData.tag);
} else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num) {
try {
I3.dispatch(`workspace number ${modelData.num}`);
@@ -1323,8 +1343,8 @@ Item {
NiriService.toggleOverview();
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
DwlService.toggleTag(root.screenName, modelData.tag);
} else if (root.isDwlLike && modelData?.tag !== undefined) {
root.dwlSvc.toggleTag(root.screenName, modelData.tag);
}
}
}
@@ -1348,7 +1368,7 @@ Item {
wsData = modelData || null;
} else if (CompositorService.isHyprland) {
wsData = modelData;
} else if (CompositorService.isDwl) {
} else if (root.isDwlLike) {
wsData = modelData;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
wsData = modelData;
@@ -1362,7 +1382,7 @@ Item {
}
if (SettingsData.showWorkspaceApps) {
if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
if (root.isDwlLike || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
} else if (CompositorService.isNiri) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
@@ -1922,8 +1942,8 @@ Item {
}
}
Connections {
target: DwlService
enabled: CompositorService.isDwl
target: root.dwlSvc
enabled: root.isDwlLike
function onStateChanged() {
delegateRoot.updateAllData();
}
@@ -70,6 +70,8 @@ Card {
// technically they might not be on mangowc, but its what we support in the docs
if (CompositorService.isDwl)
return I18n.tr("on MangoWC");
if (CompositorService.isMango)
return I18n.tr("on MangoWC");
if (CompositorService.isSway)
return I18n.tr("on Sway");
if (CompositorService.isScroll)
+7 -1
View File
@@ -227,7 +227,7 @@ Variants {
readonly property bool shouldHideForWindows: {
if (!SettingsData.dockSmartAutoHide)
return false;
if (!CompositorService.isNiri && !CompositorService.isHyprland)
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
return false;
const screenName = dock.modelData?.name ?? "";
@@ -291,6 +291,12 @@ Variants {
return false;
}
if (CompositorService.isMango) {
MangoService.windows;
MangoService.outputs;
return CompositorService.mangoDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight);
}
// Hyprland implementation (current workspace + visible special workspaces)
Hyprland.focusedWorkspace;
Hyprland.toplevels;
@@ -236,7 +236,7 @@ Item {
}
IconImage {
visible: SettingsData.dockLauncherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
visible: SettingsData.dockLauncherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
anchors.centerIn: parent
width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
@@ -249,6 +249,8 @@ Item {
return "file://" + Theme.shellDir + "/assets/hyprland.svg";
} else if (CompositorService.isDwl) {
return "file://" + Theme.shellDir + "/assets/mango.png";
} else if (CompositorService.isMango) {
return "file://" + Theme.shellDir + "/assets/mango.png";
} else if (CompositorService.isSway) {
return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isScroll) {
+2 -2
View File
@@ -429,9 +429,9 @@ MIRACLE_EOF
mango|mangowc)
require_command "mango"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg dispatch quit"
else
exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit"
exec_compositor "mango" mango -s "$QS_CMD && mmsg dispatch quit"
fi
;;
+1 -1
View File
@@ -15,7 +15,7 @@ Item {
property bool isSway: CompositorService.isSway
property bool isScroll: CompositorService.isScroll
property bool isMiracle: CompositorService.isMiracle
property bool isDwl: CompositorService.isDwl
property bool isDwl: CompositorService.isDwl || CompositorService.isMango
property bool isLabwc: CompositorService.isLabwc
property string compositorName: {
+2 -2
View File
@@ -659,7 +659,7 @@ Item {
SettingsToggleRow {
width: parent.width - parent.leftPadding
visible: CompositorService.isNiri || CompositorService.isHyprland
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
text: I18n.tr("Hide When Windows Open")
description: I18n.tr("Show the bar only when no windows are open")
checked: selectedBarConfig?.showOnWindowsOpen ?? false
@@ -1144,7 +1144,7 @@ Item {
iconName: "fit_screen"
title: I18n.tr("Maximize Detection")
description: I18n.tr("Remove gaps and border when windows are maximized")
visible: selectedBarConfig?.enabled && (CompositorService.isNiri || CompositorService.isHyprland)
visible: selectedBarConfig?.enabled && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango)
checked: selectedBarConfig?.maximizeDetection ?? true
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
maximizeDetection: checked
@@ -158,12 +158,14 @@ Singleton {
const compositorDirs = {
"niri": configDir + "/niri/dms/profiles",
"hyprland": configDir + "/hypr/dms/profiles",
"dwl": configDir + "/mango/dms/profiles"
"dwl": configDir + "/mango/dms/profiles",
"mango": configDir + "/mango/dms/profiles"
};
const compositorExts = {
"niri": ".kdl",
"hyprland": ".conf",
"dwl": ".conf"
"dwl": ".conf",
"mango": ".conf"
};
const tasks = [];
@@ -542,6 +544,14 @@ Singleton {
onWriteFailed();
});
break;
case "mango":
MangoService.generateOutputsConfig(outputsData, success => {
if (success)
onWriteSuccess();
else
onWriteFailed();
});
break;
case "dwl":
DwlService.generateOutputsConfig(outputsData, success => {
if (success)
@@ -1032,6 +1042,7 @@ Singleton {
case "hyprland":
return parseHyprlandOutputs(content);
case "dwl":
case "mango":
return parseMangoOutputs(content);
default:
return {};
@@ -1302,7 +1313,7 @@ Singleton {
params[pair.substring(0, colonIdx).trim()] = pair.substring(colonIdx + 1).trim();
}
const name = params.name;
const name = (params.name || "").replace(/^\^/, "").replace(/\$$/, "");
if (!name)
continue;
@@ -1370,6 +1381,7 @@ Singleton {
"includeLine": "require(\"dms.outputs\")"
};
case "dwl":
case "mango":
return {
"configFile": configDir + "/mango/config.conf",
"outputsFile": configDir + "/mango/dms/outputs.conf",
@@ -1383,7 +1395,7 @@ Singleton {
function checkIncludeStatus() {
const compositor = CompositorService.compositor;
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") {
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl" && compositor !== "mango") {
includeStatus = {
"exists": false,
"included": false,
@@ -1394,7 +1406,8 @@ Singleton {
}
const filename = (compositor === "niri") ? "outputs.kdl" : ((compositor === "hyprland") ? "outputs.lua" : "outputs.conf");
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
// mango and dwl both use outputs.conf under ~/.config/mango
const compositorArg = (compositor === "dwl" || compositor === "mango") ? "mangowc" : compositor;
checkingInclude = true;
Proc.runCommand("check-outputs-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => {
@@ -1569,6 +1582,9 @@ Singleton {
}
HyprlandService.generateOutputsConfig(outputsData, buildMergedHyprlandSettings());
break;
case "mango":
MangoService.generateOutputsConfig(outputsData);
break;
case "dwl":
DwlService.generateOutputsConfig(outputsData);
break;
@@ -317,7 +317,7 @@ StyledRect {
DankToggle {
width: parent.width
text: I18n.tr("Variable Refresh Rate")
visible: root.isConnected && !root.isDisabled && !CompositorService.isDwl && !CompositorService.isHyprland && !CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false)
visible: root.isConnected && !root.isDisabled && !CompositorService.isDwl && !CompositorService.isMango && !CompositorService.isHyprland && !CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false)
checked: {
const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr");
if (pendingVrr !== undefined)
@@ -500,7 +500,7 @@ Item {
Column {
id: displayFormatColumn
visible: !CompositorService.isDwl
visible: !CompositorService.isDwl && !CompositorService.isMango
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
+3 -1
View File
@@ -70,7 +70,7 @@ Item {
text: I18n.tr("Intelligent Auto-hide")
description: I18n.tr("Show dock when floating windows don't overlap its area")
checked: SettingsData.dockSmartAutoHide
visible: SettingsData.showDock && (CompositorService.isNiri || CompositorService.isHyprland)
visible: SettingsData.showDock && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango)
onToggled: checked => {
if (checked && SettingsData.dockAutoHide) {
SettingsData.set("dockAutoHide", false);
@@ -284,6 +284,8 @@ Item {
modes.push("Hyprland");
} else if (CompositorService.isDwl) {
modes.push("mango");
} else if (CompositorService.isMango) {
modes.push("mango");
} else if (CompositorService.isSway) {
modes.push("Sway");
} else if (CompositorService.isScroll) {
@@ -306,6 +306,8 @@ Item {
modes.push("Hyprland");
} else if (CompositorService.isDwl) {
modes.push("mango");
} else if (CompositorService.isMango) {
modes.push("mango");
} else if (CompositorService.isSway) {
modes.push("Sway");
} else if (CompositorService.isScroll) {
+12 -5
View File
@@ -49,6 +49,7 @@ Item {
"includeLine": "require(\"dms.cursor\")"
};
case "dwl":
case "mango":
return {
"configFile": configDir + "/mango/config.conf",
"cursorFile": configDir + "/mango/dms/cursor.conf",
@@ -62,7 +63,7 @@ Item {
function checkCursorIncludeStatus() {
const compositor = CompositorService.compositor;
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") {
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl" && compositor !== "mango") {
cursorIncludeStatus = {
"exists": false,
"included": false,
@@ -73,7 +74,7 @@ Item {
}
const filename = (compositor === "niri") ? "cursor.kdl" : ((compositor === "hyprland") ? "cursor.lua" : "cursor.conf");
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
const compositorArg = (compositor === "dwl" || compositor === "mango") ? "mangowc" : compositor;
checkingCursorInclude = true;
Proc.runCommand("check-cursor-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => {
@@ -193,7 +194,7 @@ Item {
themeColorsTab.templateDetection = JSON.parse(output.trim());
} catch (e) {}
});
if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl)
if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango)
checkCursorIncludeStatus();
}
@@ -2177,7 +2178,7 @@ Item {
title: I18n.tr("MangoWC Layout Overrides")
settingKey: "mangoLayout"
iconName: "crop_square"
visible: CompositorService.isDwl
visible: CompositorService.isDwl || CompositorService.isMango
SettingsToggleRow {
tab: "theme"
@@ -2334,7 +2335,7 @@ Item {
title: I18n.tr("Cursor Theme")
settingKey: "cursorTheme"
iconName: "mouse"
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango
Column {
width: parent.width
@@ -2490,6 +2491,8 @@ Item {
return SettingsData.cursorSettings.hyprland?.inactiveTimeout || 0;
if (CompositorService.isDwl)
return SettingsData.cursorSettings.dwl?.cursorHideTimeout || 0;
if (CompositorService.isMango)
return SettingsData.cursorSettings.mango?.cursorHideTimeout || 0;
return 0;
}
minimum: 0
@@ -2510,6 +2513,10 @@ Item {
if (!updated.dwl)
updated.dwl = {};
updated.dwl.cursorHideTimeout = newValue;
} else if (CompositorService.isMango) {
if (!updated.mango)
updated.mango = {};
updated.mango.cursorHideTimeout = newValue;
}
SettingsData.set("cursorSettings", updated);
}
+2 -2
View File
@@ -37,8 +37,8 @@ Item {
"text": I18n.tr("Layout"),
"description": I18n.tr("Display and switch DWL layouts"),
"icon": "view_quilt",
"enabled": CompositorService.isDwl && DwlService.dwlAvailable,
"warning": !CompositorService.isDwl ? I18n.tr("Requires DWL compositor") : (!DwlService.dwlAvailable ? I18n.tr("DWL service not available") : undefined)
"enabled": (CompositorService.isDwl && DwlService.dwlAvailable) || (CompositorService.isMango && MangoService.available),
"warning": CompositorService.isMango ? (!MangoService.available ? I18n.tr("DWL service not available") : undefined) : (!CompositorService.isDwl ? I18n.tr("Requires DWL compositor") : (!DwlService.dwlAvailable ? I18n.tr("DWL service not available") : undefined))
},
{
"id": "launcherButton",
+23 -9
View File
@@ -30,6 +30,7 @@ Item {
property var externalRules: []
property var activeWindows: getActiveWindows()
property string expandedExternalId: ""
readonly property string dmsRulesFileName: CompositorService.isNiri ? "dms/windowrules.kdl" : CompositorService.isMango ? "dms/windowrules.conf" : "dms/windowrules.lua"
readonly property var matchLabels: ({
"appId": I18n.tr("App ID"),
@@ -166,6 +167,13 @@ Item {
"grepPattern": "dms.windowrules",
"includeLine": "require(\"dms.windowrules\")"
};
case "mango":
return {
"configFile": configDir + "/mango/config.conf",
"rulesFile": configDir + "/mango/dms/windowrules.conf",
"grepPattern": "dms/windowrules.conf",
"includeLine": "source=./dms/windowrules.conf"
};
default:
return null;
}
@@ -173,7 +181,7 @@ Item {
function loadWindowRules() {
const compositor = CompositorService.compositor;
if (compositor !== "niri" && compositor !== "hyprland") {
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "mango") {
windowRules = [];
externalRules = [];
return;
@@ -211,11 +219,13 @@ Item {
return;
}
const compositor = CompositorService.compositor;
if (compositor !== "niri" && compositor !== "hyprland")
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "mango")
return;
Proc.runCommand("remove-windowrule", ["dms", "config", "windowrules", "remove", compositor, ruleId], (output, exitCode) => {
if (exitCode === 0) {
if (CompositorService.isMango)
MangoService.reloadConfig();
loadWindowRules();
rulesChanged();
}
@@ -231,7 +241,7 @@ Item {
return;
const compositor = CompositorService.compositor;
if (compositor !== "niri" && compositor !== "hyprland")
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "mango")
return;
let ids = windowRules.map(r => r.id);
@@ -240,6 +250,8 @@ Item {
Proc.runCommand("reorder-windowrules", ["dms", "config", "windowrules", "reorder", compositor, JSON.stringify(ids)], (output, exitCode) => {
if (exitCode === 0) {
if (CompositorService.isMango)
MangoService.reloadConfig();
loadWindowRules();
rulesChanged();
}
@@ -248,7 +260,7 @@ Item {
function checkWindowRulesIncludeStatus() {
const compositor = CompositorService.compositor;
if (compositor !== "niri" && compositor !== "hyprland") {
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "mango") {
windowRulesIncludeStatus = {
"exists": false,
"included": false,
@@ -258,7 +270,7 @@ Item {
return;
}
const filename = (compositor === "niri") ? "windowrules.kdl" : "windowrules.lua";
const filename = (compositor === "niri") ? "windowrules.kdl" : (compositor === "mango") ? "windowrules.conf" : "windowrules.lua";
checkingInclude = true;
Proc.runCommand("check-windowrules-include", ["dms", "config", "resolve-include", compositor, filename], (output, exitCode) => {
checkingInclude = false;
@@ -306,6 +318,8 @@ Item {
fixingInclude = false;
if (exitCode !== 0)
return;
if (CompositorService.isMango)
MangoService.reloadConfig();
checkWindowRulesIncludeStatus();
loadWindowRules();
});
@@ -358,7 +372,7 @@ Item {
}
Component.onCompleted: {
if (CompositorService.isNiri || CompositorService.isHyprland) {
if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango) {
checkWindowRulesIncludeStatus();
loadWindowRules();
}
@@ -415,7 +429,7 @@ Item {
}
StyledText {
text: I18n.tr("Define rules for window behavior. Saves to %1").arg(CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.lua")
text: I18n.tr("Define rules for window behavior. Saves to %1").arg(root.dmsRulesFileName)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
@@ -489,7 +503,7 @@ Item {
color: (showLegacy || showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.15) : "transparent"
border.color: (showLegacy || showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.3) : "transparent"
border.width: 1
visible: (showLegacy || showError || showSetup) && !root.checkingInclude && (CompositorService.isNiri || CompositorService.isHyprland)
visible: (showLegacy || showError || showSetup) && !root.checkingInclude && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango)
Row {
id: warningSection
@@ -519,7 +533,7 @@ Item {
}
StyledText {
readonly property string rulesFile: CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.lua"
readonly property string rulesFile: root.dmsRulesFileName
text: warningBox.showLegacy ? I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing window rules in Settings.") : (warningBox.showSetup ? I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg(rulesFile) : I18n.tr("%1 exists but is not included. Window rules won't apply.").arg(rulesFile))
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
@@ -59,7 +59,7 @@ Item {
text: I18n.tr("Show Workspace Apps")
description: I18n.tr("Display application icons in workspace indicators")
checked: SettingsData.showWorkspaceApps
visible: CompositorService.isNiri || CompositorService.isHyprland
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
onToggled: checked => SettingsData.set("showWorkspaceApps", checked)
}
@@ -151,7 +151,7 @@ Item {
text: I18n.tr("Follow Monitor Focus")
description: I18n.tr("Show workspaces of the currently focused monitor")
checked: SettingsData.workspaceFollowFocus
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
onToggled: checked => SettingsData.set("workspaceFollowFocus", checked)
}
@@ -161,7 +161,7 @@ Item {
text: I18n.tr("Show Occupied Workspaces Only")
description: I18n.tr("Display only workspaces that contain windows")
checked: SettingsData.showOccupiedWorkspacesOnly
visible: CompositorService.isNiri || CompositorService.isHyprland
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
onToggled: checked => SettingsData.set("showOccupiedWorkspacesOnly", checked)
}
@@ -171,7 +171,7 @@ Item {
text: I18n.tr("Reverse Scrolling Direction")
description: I18n.tr("Reverse workspace switch direction when scrolling over the bar")
checked: SettingsData.reverseScrolling
visible: CompositorService.isNiri || CompositorService.isHyprland
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
onToggled: checked => SettingsData.set("reverseScrolling", checked)
}
@@ -191,7 +191,7 @@ Item {
text: I18n.tr("Show All Tags")
description: I18n.tr("Show all 9 tags instead of only occupied tags (DWL only)")
checked: SettingsData.dwlShowAllTags
visible: CompositorService.isDwl
visible: CompositorService.isDwl || CompositorService.isMango
onToggled: checked => SettingsData.set("dwlShowAllTags", checked)
}
}
@@ -243,7 +243,7 @@ Item {
SettingsButtonGroupRow {
text: I18n.tr("Occupied Color")
model: ["none", "sec", "s", "sc", "sch", "schh"]
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango
buttonHeight: 22
minButtonWidth: 36
buttonPadding: Theme.spacingS
@@ -279,7 +279,7 @@ Item {
height: 1
color: Theme.outline
opacity: 0.15
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango
}
SettingsButtonGroupRow {
@@ -316,12 +316,12 @@ Item {
height: 1
color: Theme.outline
opacity: 0.15
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
}
SettingsButtonGroupRow {
text: I18n.tr("Urgent Color")
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
model: ["err", "pri", "sec", "s", "sc"]
buttonHeight: 22
minButtonWidth: 36
+101 -3
View File
@@ -16,6 +16,7 @@ Singleton {
property bool isHyprland: false
property bool isNiri: false
property bool isDwl: false
property bool isMango: false
property bool isSway: false
property bool isScroll: false
property bool isMiracle: false
@@ -29,7 +30,9 @@ Singleton {
readonly property string scrollSocket: Quickshell.env("SWAYSOCK")
readonly property string miracleSocket: Quickshell.env("MIRACLESOCK")
readonly property string labwcPid: Quickshell.env("LABWC_PID")
readonly property string mangoSignature: Quickshell.env("MANGO_INSTANCE_SIGNATURE")
property bool useNiriSorting: isNiri && NiriService
property bool useMangoSorting: isMango && MangoService
property var randrScales: ({})
property bool randrReady: false
@@ -100,6 +103,12 @@ Singleton {
return dwlScale;
}
if (isMango && screen) {
const mangoScale = MangoService.getOutputScale(screen.name);
if (mangoScale !== undefined && mangoScale > 0)
return mangoScale;
}
return screen?.devicePixelRatio || 1;
}
@@ -114,6 +123,8 @@ Singleton {
screenName = focusedWs?.monitor?.name || "";
} else if (isDwl && DwlService.activeOutput)
screenName = DwlService.activeOutput;
else if (isMango && MangoService.activeOutput)
screenName = MangoService.activeOutput;
if (!screenName)
return Quickshell.screens.length > 0 ? Quickshell.screens[0] : null;
@@ -194,6 +205,18 @@ Singleton {
}
}
Connections {
target: MangoService
function onStateChanged() {
if (isMango)
scheduleSort();
}
function onWindowsChanged() {
if (isMango)
scheduleSort();
}
}
function computeSortedToplevels() {
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values)
return [];
@@ -201,6 +224,9 @@ Singleton {
if (useNiriSorting)
return NiriService.sortToplevels(ToplevelManager.toplevels.values);
if (useMangoSorting)
return MangoService.sortToplevels(ToplevelManager.toplevels.values);
if (isHyprland)
return sortHyprlandToplevelsSafe();
@@ -697,6 +723,51 @@ Singleton {
return false;
}
// Mango clients carry absolute geometry + tags; count those on the screen's
// active tags (not minimized), made screen-relative via the monitor offset.
function mangoDockOverlapForSmartAutoHide(screenName, dockPosition, dockThickness, screenWidth, screenHeight) {
if (!isMango || !screenName || !MangoService.windows)
return false;
const out = MangoService.outputs[screenName];
const active = new Set((out?.activeTags) || []);
const monX = out?.x ?? 0;
const monY = out?.y ?? 0;
for (let i = 0; i < MangoService.windows.length; i++) {
const win = MangoService.windows[i];
if (!win || win.monitor !== screenName || win.is_minimized)
continue;
if (active.size > 0 && !(win.tags || []).some(t => active.has(t)))
continue;
const winX = (win.x ?? 0) - monX;
const winY = (win.y ?? 0) - monY;
const winW = win.width ?? 0;
const winH = win.height ?? 0;
switch (dockPosition) {
case SettingsData.Position.Top:
if (winY < dockThickness)
return true;
break;
case SettingsData.Position.Bottom:
if (winY + winH > screenHeight - dockThickness)
return true;
break;
case SettingsData.Position.Left:
if (winX < dockThickness)
return true;
break;
case SettingsData.Position.Right:
if (winX + winW > screenWidth - dockThickness)
return true;
break;
}
}
return false;
}
function filterHyprlandCurrentDisplaySafe(toplevels, screenName) {
if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels)
return toplevels;
@@ -790,15 +861,31 @@ Singleton {
NiriService.generateNiriLayoutConfig();
HyprlandService.generateLayoutConfig();
DwlService.generateLayoutConfig();
MangoService.generateLayoutConfig();
});
}
}
function detectCompositor() {
if (mangoSignature && mangoSignature.length > 0) {
isHyprland = false;
isNiri = false;
isDwl = false;
isMango = true;
isSway = false;
isScroll = false;
isMiracle = false;
isLabwc = false;
compositor = "mango";
log.info("Detected MangoWM via MANGO_INSTANCE_SIGNATURE");
return;
}
if (hyprlandSignature && hyprlandSignature.length > 0 && !niriSocket && !swaySocket && !scrollSocket && !miracleSocket && !labwcPid) {
isHyprland = true;
isNiri = false;
isDwl = false;
isMango = false;
isSway = false;
isScroll = false;
isMiracle = false;
@@ -814,6 +901,7 @@ Singleton {
isNiri = true;
isHyprland = false;
isDwl = false;
isMango = false;
isSway = false;
isScroll = false;
isMiracle = false;
@@ -849,6 +937,7 @@ Singleton {
isNiri = false;
isHyprland = false;
isDwl = false;
isMango = false;
isSway = false;
isScroll = false;
isMiracle = true;
@@ -866,6 +955,7 @@ Singleton {
isNiri = false;
isHyprland = false;
isDwl = false;
isMango = false;
isSway = false;
isScroll = true;
isMiracle = false;
@@ -881,6 +971,7 @@ Singleton {
isHyprland = false;
isNiri = false;
isDwl = false;
isMango = false;
isSway = false;
isScroll = false;
isMiracle = false;
@@ -896,6 +987,7 @@ Singleton {
isHyprland = false;
isNiri = false;
isDwl = false;
isMango = false;
isSway = false;
isScroll = false;
isMiracle = false;
@@ -908,13 +1000,15 @@ Singleton {
Connections {
target: DMSService
function onCapabilitiesReceived() {
if (!isHyprland && !isNiri && !isDwl && !isLabwc) {
if (!isHyprland && !isNiri && !isDwl && !isMango && !isLabwc) {
checkForDwl();
}
}
}
function checkForDwl() {
if (isMango)
return;
if (DMSService.apiVersion >= 12 && DMSService.capabilities.includes("dwl")) {
isHyprland = false;
isNiri = false;
@@ -935,6 +1029,8 @@ Singleton {
return HyprlandService.dpmsOff();
if (isDwl)
return _dwlPowerOffMonitors();
if (isMango)
return MangoService.powerOffMonitors();
if (isSway || isScroll || isMiracle) {
try {
I3.dispatch("output * dpms off");
@@ -954,6 +1050,8 @@ Singleton {
return HyprlandService.dpmsOn();
if (isDwl)
return _dwlPowerOnMonitors();
if (isMango)
return MangoService.powerOnMonitors();
if (isSway || isScroll || isMiracle) {
try {
I3.dispatch("output * dpms on");
@@ -975,7 +1073,7 @@ Singleton {
for (let i = 0; i < Quickshell.screens.length; i++) {
const screen = Quickshell.screens[i];
if (screen && screen.name) {
Quickshell.execDetached(["mmsg", "-d", "disable_monitor," + screen.name]);
Quickshell.execDetached(["mmsg", "dispatch", "disable_monitor," + screen.name]);
}
}
}
@@ -989,7 +1087,7 @@ Singleton {
for (let i = 0; i < Quickshell.screens.length; i++) {
const screen = Quickshell.screens[i];
if (screen && screen.name) {
Quickshell.execDetached(["mmsg", "-d", "enable_monitor," + screen.name]);
Quickshell.execDetached(["mmsg", "dispatch", "enable_monitor," + screen.name]);
}
}
}
+11 -13
View File
@@ -21,6 +21,8 @@ Singleton {
property int _lastGapValue: -1
property bool dwlAvailable: false
// Alias so consumers can treat DwlService/MangoService uniformly via `.available`.
readonly property bool available: dwlAvailable
property var outputs: ({})
property var tagCount: 9
property var layouts: []
@@ -233,27 +235,23 @@ Singleton {
}
function quit() {
Quickshell.execDetached(["mmsg", "-d", "quit"]);
Quickshell.execDetached(["mmsg", "dispatch", "quit"]);
}
Process {
id: scaleQueryProcess
command: ["mmsg", "-A"]
command: ["mmsg", "get", "all-monitors"]
running: false
stdout: StdioCollector {
onStreamFinished: {
try {
const newScales = {};
const lines = text.trim().split('\n');
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 3 && parts[1] === "scale_factor") {
const outputName = parts[0];
const scale = parseFloat(parts[2]);
if (!isNaN(scale)) {
newScales[outputName] = scale;
}
const data = JSON.parse(text.trim());
const monitors = data.monitors || [];
for (const mon of monitors) {
if (mon.name && typeof mon.scale === "number" && mon.scale > 0) {
newScales[mon.name] = mon.scale;
}
}
outputScales = newScales;
@@ -327,7 +325,7 @@ Singleton {
const transform = transformToMango(output.logical?.transform ?? "Normal");
const vrr = output.vrr_enabled ? 1 : 0;
const rule = ["name:" + outputName, "width:" + width, "height:" + height, "refresh:" + refreshRate, "x:" + x, "y:" + y, "scale:" + scale, "rr:" + transform, "vrr:" + vrr].join(",");
const rule = ["name:^" + outputName + "$", "width:" + width, "height:" + height, "refresh:" + refreshRate, "x:" + x, "y:" + y, "scale:" + scale, "rr:" + transform, "vrr:" + vrr].join(",");
lines.push("monitorrule=" + rule);
}
@@ -352,7 +350,7 @@ Singleton {
}
function reloadConfig() {
Proc.runCommand("mango-reload", ["mmsg", "-d", "reload_config"], (output, exitCode) => {
Proc.runCommand("mango-reload", ["mmsg", "dispatch", "reload_config"], (output, exitCode) => {
if (exitCode !== 0)
log.warn("mmsg reload_config failed:", output);
});
+10 -4
View File
@@ -14,13 +14,13 @@ Singleton {
id: root
readonly property var log: Log.scoped("KeybindsService")
property bool available: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl
property bool available: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango
property string currentProvider: {
if (CompositorService.isNiri)
return "niri";
if (CompositorService.isHyprland)
return "hyprland";
if (CompositorService.isDwl)
if (CompositorService.isDwl || CompositorService.isMango)
return "mangowc";
return "";
}
@@ -30,7 +30,7 @@ Singleton {
return "niri";
if (CompositorService.isHyprland)
return "hyprland";
if (CompositorService.isDwl)
if (CompositorService.isDwl || CompositorService.isMango)
return "mangowc";
return "";
}
@@ -118,7 +118,7 @@ Singleton {
Connections {
target: CompositorService
function onCompositorChanged() {
if (!CompositorService.isNiri)
if (!CompositorService.isNiri && !CompositorService.isMango)
return;
Qt.callLater(root.loadBinds);
}
@@ -203,6 +203,8 @@ Singleton {
}
root.lastError = "";
root.bindSaveCompleted(true);
if (CompositorService.isMango)
MangoService.reloadConfig();
root.loadBinds(false);
}
}
@@ -226,6 +228,8 @@ Singleton {
return;
}
root.lastError = "";
if (CompositorService.isMango)
MangoService.reloadConfig();
root.loadBinds(false);
}
}
@@ -254,6 +258,8 @@ Singleton {
root.dmsBindsFixed();
const bindsRel = root.currentProvider === "niri" ? "dms/binds.kdl" : root.currentProvider === "hyprland" ? "dms/binds.lua" : "dms/binds.conf";
ToastService.showInfo(I18n.tr("Binds include added"), I18n.tr("%1 is now included in config").arg(bindsRel), "", "keybinds");
if (CompositorService.isMango)
MangoService.reloadConfig();
Qt.callLater(root.forceReload);
}
}
+561
View File
@@ -0,0 +1,561 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
// Native MangoWM IPC client. mango advertises a JSON-over-Unix-socket protocol
// via MANGO_INSTANCE_SIGNATURE; each connection issues one `watch <target>` verb
// and gets a full JSON snapshot followed by newline-delimited updates. Replaces
// the legacy dwl-ipc-v2 path (DwlService) for mango, exposing a
// DwlService-compatible tag API plus a per-client window list.
Singleton {
id: root
readonly property var log: Log.scoped("MangoService")
readonly property string socketPath: Quickshell.env("MANGO_INSTANCE_SIGNATURE")
readonly property bool available: socketPath.length > 0
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string mangoDmsDir: configDir + "/mango/dms"
readonly property string outputsPath: mangoDmsDir + "/outputs.conf"
readonly property string layoutPath: mangoDmsDir + "/layout.conf"
readonly property string cursorPath: mangoDmsDir + "/cursor.conf"
property int _lastGapValue: -1
// name -> { name, active, x, y, width, height, scale, layoutIndex,
// layoutSymbol, lastOpenSurface, kbLayout, keymode,
// tags: [{ tag, state, clients, focused, urgent, layout }] }
property var outputs: ({})
property string activeOutput: ""
property int tagCount: 9
property var displayScales: ({})
property string currentKeyboardLayout: ""
// Rich client list from `watch all-clients` (mango "clients").
property var windows: []
// windowsChanged is auto-generated by the `windows` property's change signal.
signal stateChanged
// State sockets
// One connection per watch target; mango streams a fresh full snapshot on
// every change, so each line is treated as the complete state.
DankSocket {
id: monitorsSocket
path: root.socketPath
connected: root.available
onConnectionStateChanged: {
if (connected)
send("watch all-monitors");
}
parser: SplitParser {
onRead: line => root._handleMonitors(line)
}
}
DankSocket {
id: clientsSocket
path: root.socketPath
connected: root.available
onConnectionStateChanged: {
if (connected)
send("watch all-clients");
}
parser: SplitParser {
onRead: line => root._handleClients(line)
}
}
function _handleMonitors(line) {
if (!line || !line.trim())
return;
let data;
try {
data = JSON.parse(line);
} catch (e) {
log.warn("Failed to parse all-monitors:", e);
return;
}
const monitors = data.monitors;
if (!Array.isArray(monitors))
return;
const newOutputs = {};
const newScales = {};
let newActive = "";
let newTagCount = root.tagCount;
let newKbLayout = root.currentKeyboardLayout;
for (const m of monitors) {
if (!m.name)
continue;
const tags = (m.tags || []).map(t => ({
// 0-based to match the legacy dwl tag model used by consumers
"tag": (t.index ?? 1) - 1,
"state": t.is_urgent ? 2 : (t.is_active ? 1 : 0),
"clients": t.client_count ?? 0,
"focused": !!t.is_active,
"urgent": !!t.is_urgent,
"layout": t.layout ?? ""
}));
newOutputs[m.name] = {
"name": m.name,
"active": !!m.active,
"x": m.x ?? 0,
"y": m.y ?? 0,
"width": m.width ?? 0,
"height": m.height ?? 0,
"scale": m.scale ?? 1.0,
"layoutIndex": m.layout_index ?? 0,
"layout": m.layout_index ?? 0,
"activeTags": m.active_tags || [],
"layoutSymbol": m.layout_symbol ?? "",
"lastOpenSurface": m.last_open_surface ?? "",
"keymode": m.keymode ?? "",
"kbLayout": m.keyboardlayout ?? "",
"tags": tags
};
if (typeof m.scale === "number" && m.scale > 0)
newScales[m.name] = m.scale;
if (m.active) {
newActive = m.name;
if (m.keyboardlayout)
newKbLayout = m.keyboardlayout;
}
if (tags.length > 0)
newTagCount = tags.length;
}
root.outputs = newOutputs;
root.displayScales = newScales;
root.tagCount = newTagCount;
if (newActive)
root.activeOutput = newActive;
root.currentKeyboardLayout = newKbLayout;
root.stateChanged();
}
function _handleClients(line) {
if (!line || !line.trim())
return;
let data;
try {
data = JSON.parse(line);
} catch (e) {
log.warn("Failed to parse all-clients:", e);
return;
}
if (!Array.isArray(data.clients))
return;
root.windows = data.clients;
}
// DwlService-compatible tag API
function getOutputState(outputName) {
return (outputs && outputs[outputName]) ? outputs[outputName] : null;
}
function getActiveTags(outputName) {
const output = getOutputState(outputName);
if (!output || !output.tags)
return [];
return output.tags.filter(tag => tag.state === 1).map(tag => tag.tag);
}
function getTagsWithClients(outputName) {
const output = getOutputState(outputName);
if (!output || !output.tags)
return [];
return output.tags.filter(tag => tag.clients > 0).map(tag => tag.tag);
}
function getUrgentTags(outputName) {
const output = getOutputState(outputName);
if (!output || !output.tags)
return [];
return output.tags.filter(tag => tag.state === 2).map(tag => tag.tag);
}
function getVisibleTags(outputName) {
const output = getOutputState(outputName);
if (!output || !output.tags)
return [];
const visibleTags = new Set();
output.tags.forEach(tag => {
if (tag.state === 1 || tag.clients > 0)
visibleTags.add(tag.tag);
});
return Array.from(visibleTags).sort((a, b) => a - b);
}
function getOutputScale(outputName) {
return displayScales[outputName];
}
// Window list wlr toplevel matching (per-tag sort/filter)
// Match mango clients to wlr foreign-toplevels by appId+title to enrich them
// with owning tags/monitor for per-tag filtering and stable ordering.
function _screenName(screenOrName) {
return (typeof screenOrName === "string") ? screenOrName : (screenOrName?.name ?? "");
}
function _orderedClients() {
const list = (windows || []).slice();
list.sort((a, b) => {
const ma = outputs[a.monitor], mb = outputs[b.monitor];
const ax = ma?.x ?? 1e9, ay = ma?.y ?? 1e9;
const bx = mb?.x ?? 1e9, by = mb?.y ?? 1e9;
if (ax !== bx)
return ax - bx;
if (ay !== by)
return ay - by;
if ((a.y ?? 0) !== (b.y ?? 0))
return (a.y ?? 0) - (b.y ?? 0);
if ((a.x ?? 0) !== (b.x ?? 0))
return (a.x ?? 0) - (b.x ?? 0);
return (a.id ?? 0) - (b.id ?? 0);
});
return list;
}
function _matchAndEnrich(toplevels, clients) {
const used = new Set();
const result = [];
for (const client of clients) {
let bestMatch = null;
let bestScore = -1;
for (const toplevel of toplevels) {
if (used.has(toplevel))
continue;
if (toplevel.appId !== client.appid)
continue;
let score = 1;
if (client.title && toplevel.title) {
if (toplevel.title === client.title)
score = 3;
else if (toplevel.title.includes(client.title) || client.title.includes(toplevel.title))
score = 2;
}
if (score > bestScore) {
bestScore = score;
bestMatch = toplevel;
if (score === 3)
break;
}
}
if (!bestMatch)
continue;
used.add(bestMatch);
const enriched = {
"appId": bestMatch.appId,
"title": bestMatch.title,
"activated": !!client.is_focused,
"mangoWindowId": client.id,
"mangoTags": client.tags || [],
"mangoMonitor": client.monitor
};
for (let prop in bestMatch) {
if (!(prop in enriched))
enriched[prop] = bestMatch[prop];
}
result.push(enriched);
}
return result;
}
function sortToplevels(toplevels) {
if (!toplevels || toplevels.length === 0 || windows.length === 0)
return [...toplevels];
const enriched = _matchAndEnrich(toplevels, _orderedClients());
const used = new Set(enriched.map(e => e.mangoWindowId));
// Append wlr toplevels that had no mango client match (rare).
const matchedTitles = new Set(enriched.map(e => e.title + "\u0000" + e.appId));
for (const t of toplevels) {
if (!matchedTitles.has((t.title || "") + "\u0000" + (t.appId || "")))
enriched.push(t);
}
return enriched;
}
function _activeTagSet(screenName) {
const out = outputs[screenName];
return new Set((out?.activeTags) || []);
}
function filterCurrentWorkspace(toplevels, screenOrName) {
const screenName = _screenName(screenOrName);
if (!screenName)
return toplevels;
const active = _activeTagSet(screenName);
if (active.size === 0)
return toplevels;
const onActive = tags => (tags || []).some(t => active.has(t));
if (toplevels.length > 0 && toplevels[0].mangoTags !== undefined)
return toplevels.filter(t => t.mangoMonitor === screenName && onActive(t.mangoTags));
const clients = (windows || []).filter(c => c.monitor === screenName && onActive(c.tags));
return _matchAndEnrich(toplevels, clients);
}
function filterCurrentDisplay(toplevels, screenOrName) {
const screenName = _screenName(screenOrName);
if (!toplevels || toplevels.length === 0 || !screenName)
return toplevels;
if (toplevels.length > 0 && toplevels[0].mangoMonitor !== undefined)
return toplevels.filter(t => t.mangoMonitor === screenName);
const clients = (windows || []).filter(c => c.monitor === screenName);
return _matchAndEnrich(toplevels, clients);
}
// Commands (mango verb IPC: mmsg dispatch <func>,<args>)
function reloadConfig() {
Proc.runCommand("mango-reload", ["mmsg", "dispatch", "reload_config"], (output, exitCode) => {
if (exitCode !== 0)
log.warn("mmsg reload_config failed:", output);
});
}
function quit() {
Quickshell.execDetached(["mmsg", "dispatch", "quit"]);
}
// mango tag dispatches act on the focused monitor; tagIndex is 0-based
// (dwl model), mango `view`/`toggleview` take a 1-based tag number.
function switchToTag(outputName, tagIndex) {
Quickshell.execDetached(["mmsg", "dispatch", "view," + (tagIndex + 1)]);
}
function toggleTag(outputName, tagIndex) {
Quickshell.execDetached(["mmsg", "dispatch", "toggleview," + (tagIndex + 1)]);
}
// mango's tiling layouts are a fixed compiled-in set the IPC doesn't expose,
// so mirror it here in mango's layouts[] order (layout_index aligns). The
// parallel name list exists because `setlayout` dispatches by name, not index.
readonly property var layouts: ["T", "S", "G", "M", "K", "CT", "RT", "VS", "VT", "VG", "VK", "DW", "F", "VF"]
readonly property var _layoutNames: ["tile", "scroller", "grid", "monocle", "deck", "center_tile", "right_tile", "vertical_scroller", "vertical_tile", "vertical_grid", "vertical_deck", "dwindle", "fair", "vertical_fair"]
function setLayout(outputName, index) {
const name = _layoutNames[index];
if (name)
Quickshell.execDetached(["mmsg", "dispatch", "setlayout," + name]);
}
function cycleKeyboardLayout() {
Quickshell.execDetached(["mmsg", "dispatch", "switch_keyboard_layout"]);
}
function powerOffMonitors() {
const screens = Quickshell.screens || [];
for (let i = 0; i < screens.length; i++) {
if (screens[i] && screens[i].name)
Quickshell.execDetached(["mmsg", "dispatch", "disable_monitor," + screens[i].name]);
}
}
function powerOnMonitors() {
const screens = Quickshell.screens || [];
for (let i = 0; i < screens.length; i++) {
if (screens[i] && screens[i].name)
Quickshell.execDetached(["mmsg", "dispatch", "enable_monitor," + screens[i].name]);
}
}
// Config generation (mango config fragments under ~/.config/mango/dms)
Connections {
target: SettingsData
function onBarConfigsChanged() {
if (!CompositorService.isMango)
return;
const newGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
if (newGaps === root._lastGapValue)
return;
root._lastGapValue = newGaps;
generateLayoutConfig();
}
}
Connections {
target: CompositorService
function onIsMangoChanged() {
if (CompositorService.isMango)
generateLayoutConfig();
}
}
function transformToMango(transform) {
switch (transform) {
case "Normal":
return 0;
case "90":
return 1;
case "180":
return 2;
case "270":
return 3;
case "Flipped":
return 4;
case "Flipped90":
return 5;
case "Flipped180":
return 6;
case "Flipped270":
return 7;
default:
return 0;
}
}
function generateOutputsConfig(outputsData, callback) {
if (!outputsData || Object.keys(outputsData).length === 0) {
if (callback)
callback(false);
return;
}
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
for (const outputName in outputsData) {
const output = outputsData[outputName];
if (!output)
continue;
let width = 1920;
let height = 1080;
let refreshRate = 60;
if (output.modes && output.current_mode !== undefined) {
const mode = output.modes[output.current_mode];
if (mode) {
width = mode.width || 1920;
height = mode.height || 1080;
refreshRate = Math.round((mode.refresh_rate || 60000) / 1000);
}
}
const x = output.logical?.x ?? 0;
const y = output.logical?.y ?? 0;
const scale = output.logical?.scale ?? 1.0;
const transform = transformToMango(output.logical?.transform ?? "Normal");
const vrr = output.vrr_enabled ? 1 : 0;
// Anchor the name regex: mango matches `name:` unanchored (first-match
// wins), so a bare "DP-1" would also match "eDP-1" and collapse outputs.
const rule = ["name:^" + outputName + "$", "width:" + width, "height:" + height, "refresh:" + refreshRate, "x:" + x, "y:" + y, "scale:" + scale, "rr:" + transform, "vrr:" + vrr].join(",");
lines.push("monitorrule=" + rule);
}
lines.push("");
const content = lines.join("\n");
Proc.runCommand("mango-write-outputs", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
log.warn("Failed to write outputs config:", output);
if (callback)
callback(false);
return;
}
log.info("Generated outputs config at", outputsPath);
if (CompositorService.isMango)
reloadConfig();
if (callback)
callback(true);
});
}
function generateLayoutConfig() {
if (!CompositorService.isMango)
return;
const defaultRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
const defaultBorderSize = 2;
const cornerRadius = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutRadiusOverride >= 0) ? SettingsData.mangoLayoutRadiusOverride : defaultRadius;
const gaps = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutGapsOverride >= 0) ? SettingsData.mangoLayoutGapsOverride : defaultGaps;
const borderSize = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutBorderSize >= 0) ? SettingsData.mangoLayoutBorderSize : defaultBorderSize;
let content = `# Auto-generated by DMS - do not edit manually
border_radius=${cornerRadius}
gappih=${gaps}
gappiv=${gaps}
gappoh=${gaps}
gappov=${gaps}
borderpx=${borderSize}
`;
Proc.runCommand("mango-write-layout", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${layoutPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
log.warn("Failed to write layout config:", output);
return;
}
log.info("Generated layout config at", layoutPath);
reloadConfig();
});
}
function generateCursorConfig() {
if (!CompositorService.isMango)
return;
const settings = typeof SettingsData !== "undefined" ? SettingsData.cursorSettings : null;
if (!settings) {
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
if (exitCode !== 0)
log.warn("Failed to write cursor config:", output);
});
return;
}
const themeName = settings.theme === "System Default" ? (SettingsData.systemDefaultCursorTheme || "") : settings.theme;
const size = settings.size || 24;
const hideTimeout = settings.mango?.cursorHideTimeout || 0;
const isDefaultConfig = !themeName && size === 24 && hideTimeout === 0;
if (isDefaultConfig) {
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
if (exitCode !== 0)
log.warn("Failed to write cursor config:", output);
});
return;
}
let content = `# Auto-generated by DMS - do not edit manually
cursor_size=${size}`;
if (themeName)
content += `\ncursor_theme=${themeName}`;
if (hideTimeout > 0)
content += `\ncursor_hide_timeout=${hideTimeout}`;
content += `\n`;
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${cursorPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
log.warn("Failed to write cursor config:", output);
return;
}
log.info("Generated cursor config at", cursorPath);
reloadConfig();
});
}
}
+5
View File
@@ -319,6 +319,11 @@ Singleton {
return;
}
if (CompositorService.isMango) {
MangoService.quit();
return;
}
if (CompositorService.isLabwc) {
LabwcService.quit();
return;
@@ -36,6 +36,7 @@ Singleton {
"isNiri": () => CompositorService.isNiri,
"isHyprland": () => CompositorService.isHyprland,
"isDwl": () => CompositorService.isDwl,
"isMango": () => CompositorService.isMango,
"keybindsAvailable": () => KeybindsService.available,
"soundsAvailable": () => AudioService.soundsAvailable,
"cupsAvailable": () => CupsService.cupsAvailable,
+1 -1
View File
@@ -1,4 +1,4 @@
[templates.dmsmango]
input_path = 'SHELL_DIR/matugen/templates/mango-colors.conf'
output_path = 'CONFIG_DIR/mango/dms/colors.conf'
post_hook = 'sh -c "mmsg -d reload_config 2>&1 || true"'
post_hook = 'sh -c "mmsg dispatch reload_config 2>&1 || true"'