From c65cda2273a4f95323390d3d3027d09cae7c2d97 Mon Sep 17 00:00:00 2001 From: purian23 Date: Sun, 20 Sep 2026 15:18:54 -0400 Subject: [PATCH] mango: discover layouts and fix monitor targeting and visibility Closes #2924 --- quickshell/Common/WindowModel.js | 2 + quickshell/Services/MangoService.qml | 55 ++++++++++++++++++++------ quickshell/tests/window-model.test.mjs | 13 ++++++ 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/quickshell/Common/WindowModel.js b/quickshell/Common/WindowModel.js index 5d921ef21..a2f62033f 100644 --- a/quickshell/Common/WindowModel.js +++ b/quickshell/Common/WindowModel.js @@ -164,6 +164,8 @@ function mangoVisibleWindows(windows, output, screenName) { return windows.filter(win => { if (!win || win.monitor !== screenName || win.is_minimized) return false; + if (typeof win.is_visible === "boolean") + return win.is_visible; return active.size === 0 || (win.tags || []).some(t => active.has(t)); }); } diff --git a/quickshell/Services/MangoService.qml b/quickshell/Services/MangoService.qml index cc02f0b04..277b0953f 100644 --- a/quickshell/Services/MangoService.qml +++ b/quickshell/Services/MangoService.qml @@ -107,8 +107,10 @@ Singleton { connected: root.available onConnectionStateChanged: { - if (connected) + if (linkUp) { send("watch all-monitors"); + root.refreshLayouts(); + } } parser: SplitParser { @@ -122,7 +124,7 @@ Singleton { connected: root.available onConnectionStateChanged: { - if (connected) + if (linkUp) send("watch all-clients"); } @@ -161,6 +163,10 @@ Singleton { property bool _dispatchConnectionUsed: false function dispatch(command, callback) { + request("dispatch " + command, callback); + } + + function request(command, callback) { if (!root.available) return; _dispatchQueue.push({ @@ -175,7 +181,7 @@ Singleton { return; _dispatchConnectionUsed = true; _dispatchInFlight = _dispatchQueue.shift(); - dispatchSocket.send("dispatch " + _dispatchInFlight.command); + dispatchSocket.send(_dispatchInFlight.command); } function _onDispatchReply(line) { @@ -472,6 +478,7 @@ Singleton { } if (shouldShowToast) ToastService.showInfo(I18n.tr("mango: config reloaded"), "", "", "mango-config"); + root.refreshLayouts(); }); } @@ -479,26 +486,48 @@ Singleton { 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 dispatchOnOutput(outputName, command) { + if (!outputName || !getOutputState(outputName)) + return; + // Always queue the focus: IPC snapshots can lag behind earlier commands. + dispatch("focusmon," + outputName); + dispatch(command); + } + + // The dwl model is 0-based; Mango view/toggleview take a 1-based tag number. function switchToTag(outputName, tagIndex) { - dispatch("view," + (tagIndex + 1)); + dispatchOnOutput(outputName, "view," + (tagIndex + 1)); } function toggleTag(outputName, tagIndex) { - dispatch("toggleview," + (tagIndex + 1)); + dispatchOnOutput(outputName, "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"] + // Mango before 0.16.1 has no `get layouts`; retain its compiled-in order. + property var _availableLayouts: null + readonly property var layouts: _availableLayouts ? _availableLayouts.map(layout => layout.symbol) : ["T", "S", "G", "M", "K", "CT", "RT", "VS", "VT", "VG", "VK", "DW", "F", "VF"] + readonly property var _layoutNames: _availableLayouts ? _availableLayouts.map(layout => layout.name) : ["tile", "scroller", "grid", "monocle", "deck", "center_tile", "right_tile", "vertical_scroller", "vertical_tile", "vertical_grid", "vertical_deck", "dwindle", "fair", "vertical_fair"] + + function refreshLayouts() { + request("get layouts", line => { + let data; + try { + data = JSON.parse(line); + } catch (e) { + return; + } + if (!Array.isArray(data?.layouts) || data.layouts.length === 0) + return; + if (!data.layouts.every(layout => typeof layout?.name === "string" && layout.name.length > 0 && typeof layout.symbol === "string")) + return; + root._availableLayouts = data.layouts; + }); + } function setLayout(outputName, index) { const name = _layoutNames[index]; if (name) - dispatch("setlayout," + name); + dispatchOnOutput(outputName, "setlayout," + name); } function cycleKeyboardLayout() { diff --git a/quickshell/tests/window-model.test.mjs b/quickshell/tests/window-model.test.mjs index 65cb804ea..5baf8721a 100644 --- a/quickshell/tests/window-model.test.mjs +++ b/quickshell/tests/window-model.test.mjs @@ -174,6 +174,19 @@ test("hyprland: workspace membership against the focused workspace, pid from the assert.equal(specialWorkspaceName(hypr, null), ""); }); +test("mango: compositor visibility covers global windows and overview without counting hidden clients", () => { + const windows = [ + { id: 1, monitor: "DP-1", tags: [2], is_visible: true }, + { id: 2, monitor: "DP-1", tags: [1], is_visible: false }, + { id: 3, monitor: "DP-2", tags: [1], is_visible: true }, + { id: 4, monitor: "DP-1", tags: [1], is_visible: true, is_minimized: true }, + { id: 5, monitor: "DP-1", tags: [1] } + ]; + const visible = activeTags => Array.from(model.mangoVisibleWindows(windows, { activeTags }, "DP-1"), win => win.id); + assert.deepEqual(visible([1]), [1, 5]); + assert.deepEqual(visible([0]), [1]); +}); + test("mango: pid by window id, overlap and maximized only count visible windows on the output's active tags", () => { const m = fixture("mango"); assert.equal(mangoPid(m.twoOutputs.windows, { mangoWindowId: 1 }), 201);