From 12e54e0225fd9b01c54a6b07e79e7d1f0e2ef76e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s?= <690217+tomups@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:54:58 +0200 Subject: [PATCH] Keep MangoWM workspace apps on their own monitor (#3832) port 2.0 (cherry picked from commit 203b1369b3b157c8270822e8cc84f5a3849f4b81) --- quickshell/Common/WorkspaceModel.js | 2 +- quickshell/Services/CompositorService.qml | 2 +- .../tests/fixtures/workspaces/mango.json | 4 ++-- quickshell/tests/window-model.test.mjs | 8 ++++++++ quickshell/tests/workspace-model.test.mjs | 19 +++++++++++++++++-- 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/quickshell/Common/WorkspaceModel.js b/quickshell/Common/WorkspaceModel.js index cb6c59300..2a2bfa168 100644 --- a/quickshell/Common/WorkspaceModel.js +++ b/quickshell/Common/WorkspaceModel.js @@ -439,7 +439,7 @@ function mangoActiveWorkspace(raw, screenName) { function mangoWindowsOnWorkspace(windows, workspace) { // mangoTags are 1-based; workspace.id is 0-based. - return windows.filter(win => win && (win.mangoTags || []).includes(workspace.id + 1)); + return windows.filter(win => win && win.mangoMonitor === workspace.output && (win.mangoTags || []).includes(workspace.id + 1)); } function i3Key(ws) { diff --git a/quickshell/Services/CompositorService.qml b/quickshell/Services/CompositorService.qml index ced030d92..94b78b5e2 100644 --- a/quickshell/Services/CompositorService.qml +++ b/quickshell/Services/CompositorService.qml @@ -468,7 +468,7 @@ Singleton { const yKey = y.niriWindowId !== undefined ? y.niriWindowId : y.mangoWindowId; if (xKey === undefined || yKey === undefined) return false; - if (xKey !== yKey || x.niriWorkspaceId !== y.niriWorkspaceId || x.appId !== y.appId || x.title !== y.title || !!x.activated !== !!y.activated || !!x.fullscreen !== !!y.fullscreen || !!x.maximized !== !!y.maximized || !!x.minimized !== !!y.minimized) + if (xKey !== yKey || x.niriWorkspaceId !== y.niriWorkspaceId || x.mangoMonitor !== y.mangoMonitor || x.appId !== y.appId || x.title !== y.title || !!x.activated !== !!y.activated || !!x.fullscreen !== !!y.fullscreen || !!x.maximized !== !!y.maximized || !!x.minimized !== !!y.minimized) return false; } return true; diff --git a/quickshell/tests/fixtures/workspaces/mango.json b/quickshell/tests/fixtures/workspaces/mango.json index 45c3212f1..d8e84253a 100644 --- a/quickshell/tests/fixtures/workspaces/mango.json +++ b/quickshell/tests/fixtures/workspaces/mango.json @@ -36,8 +36,8 @@ } }, "windows": [ - { "appId": "alacritty", "mangoTags": [3] }, - { "appId": "firefox", "mangoTags": [2, 3] }, + { "appId": "alacritty", "mangoMonitor": "DP-1", "mangoTags": [3] }, + { "appId": "firefox", "mangoMonitor": "DP-1", "mangoTags": [2, 3] }, { "appId": "untagged" } ] }, diff --git a/quickshell/tests/window-model.test.mjs b/quickshell/tests/window-model.test.mjs index 8b2e44392..9a76264ca 100644 --- a/quickshell/tests/window-model.test.mjs +++ b/quickshell/tests/window-model.test.mjs @@ -20,6 +20,14 @@ const aqueousService = source("../Services/AqueousService.qml"); const model = vm.createContext({}); vm.runInContext(source("../Common/WindowModel.js").replace(/^\.pragma.*$/m, ""), model); +test("mango monitor changes invalidate cached toplevels", () => { + const context = vm.createContext({}); + lift(compositorService, "_toplevelListEquivalent", context); + const window = { mangoWindowId: 1, appId: "app1", mangoMonitor: "DP-1", mangoTags: [1] }; + assert.equal(context._toplevelListEquivalent([window], [{ ...window }]), true); + assert.equal(context._toplevelListEquivalent([window], [{ ...window, mangoMonitor: "HDMI-A-1" }]), false); +}); + function niriState(scenario) { const s = fixture("niri")[scenario]; const toplevels = s.toplevels.map(t => ({ ...t })); diff --git a/quickshell/tests/workspace-model.test.mjs b/quickshell/tests/workspace-model.test.mjs index 783ff46a0..f55c3c0df 100644 --- a/quickshell/tests/workspace-model.test.mjs +++ b/quickshell/tests/workspace-model.test.mjs @@ -291,8 +291,23 @@ test("mango scroll targets, active tag and windows", () => { assert.deepEqual(plain(model.mangoActiveWorkspace(dp1, "DP-1")), { id: 2, idx: 3, name: "", output: "DP-1", active: true, placeholder: false }); assert.equal(model.mangoActiveWorkspace(mango("available", "HDMI-A-1"), "HDMI-A-1").id, 0); assert.equal(model.mangoActiveWorkspace(mango("available", "DP-2"), "DP-2"), null); - assert.deepEqual(pick(model.mangoWindowsOnWorkspace(dp1.windows, { id: 2 }), "appId"), ["alacritty", "firefox"]); - assert.deepEqual(pick(model.mangoWindowsOnWorkspace(dp1.windows, { id: 1 }), "appId"), ["firefox"]); + assert.deepEqual(pick(model.mangoWindowsOnWorkspace(dp1.windows, { id: 2, output: "DP-1" }), "appId"), ["alacritty", "firefox"]); + assert.deepEqual(pick(model.mangoWindowsOnWorkspace(dp1.windows, { id: 1, output: "DP-1" }), "appId"), ["firefox"]); +}); + +test("mango workspace apps follow their output when windows share a tag", () => { + const windows = [ + { appId: "app1", mangoMonitor: "DP-1", mangoTags: [1] }, + { appId: "app2", mangoMonitor: "HDMI-A-1", mangoTags: [1] } + ]; + const dp1 = model.mangoRecord(0, {}, "DP-1"); + const hdmi = model.mangoRecord(0, {}, "HDMI-A-1"); + assert.deepEqual(pick(model.mangoWindowsOnWorkspace(windows, dp1), "appId"), ["app1"]); + assert.deepEqual(pick(model.mangoWindowsOnWorkspace(windows, hdmi), "appId"), ["app2"]); + + windows[0].mangoMonitor = "HDMI-A-1"; + assert.deepEqual(pick(model.mangoWindowsOnWorkspace(windows, dp1), "appId"), []); + assert.deepEqual(pick(model.mangoWindowsOnWorkspace(windows, hdmi), "appId"), ["app1", "app2"]); }); test("i3 lists order numbered before named and strip the number prefix", () => {