From 8ea80e0f904792adb6fd8ffe76ea56ab45444374 Mon Sep 17 00:00:00 2001 From: Konstantin Merkel Date: Sun, 7 Jun 2026 15:35:36 +0200 Subject: [PATCH 1/2] bugfix: implemented Expected State Cache to avoid race-conditions --- lib/controller.js | 16 +++++++++------- lib/monitor.js | 14 +++++++++++--- lib/window.js | 26 ++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/lib/controller.js b/lib/controller.js index c345d2c..8ce7d5e 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -134,8 +134,8 @@ export class TilingController { } _resolveTilingContext(window, wrapper) { - let workspace = window.get_workspace ? window.get_workspace() : null; - let monitorIndex = window.get_monitor ? window.get_monitor() : -1; + let workspace = wrapper ? wrapper.effectiveWorkspace : (window.get_workspace ? window.get_workspace() : null); + let monitorIndex = wrapper ? wrapper.effectiveMonitorIndex : (window.get_monitor ? window.get_monitor() : -1); if (!workspace) workspace = global.workspace_manager.get_active_workspace(); if (monitorIndex < 0) monitorIndex = global.display.get_current_monitor(); @@ -146,7 +146,7 @@ export class TilingController { const preferredSlot = isRestoring ? this._restoringWindows.get(window) : undefined; if (isRestoring) { monitorIndex = wrapper.monitorIndex; - let currentMon = window.get_monitor ? window.get_monitor() : -1; + let currentMon = wrapper ? wrapper.effectiveMonitorIndex : (window.get_monitor ? window.get_monitor() : -1); if (!window.minimized && currentMon === wrapper.monitorIndex) { this._restoringWindows.delete(window); } @@ -324,10 +324,11 @@ export class TilingController { */ moveWindowDirection(window, direction) { if (!window) return; - const workspace = window.get_workspace(); + const wrapper = this._windowWrappers.get(window); + const workspace = wrapper ? wrapper.effectiveWorkspace : window.get_workspace(); if (!workspace) return; - const monitorIndex = window.get_monitor(); + const monitorIndex = wrapper ? wrapper.effectiveMonitorIndex : window.get_monitor(); const monitorId = this.monitorManager.getMonitorId(monitorIndex); const layout = this.workspaceManager.getLayout(workspace); @@ -338,10 +339,11 @@ export class TilingController { focusWindowDirection(window, direction) { if (!window) return; - const workspace = window.get_workspace(); + const wrapper = this._windowWrappers.get(window); + const workspace = wrapper ? wrapper.effectiveWorkspace : window.get_workspace(); if (!workspace) return; - const monitorIndex = window.get_monitor(); + const monitorIndex = wrapper ? wrapper.effectiveMonitorIndex : window.get_monitor(); const monitorId = this.monitorManager.getMonitorId(monitorIndex); const layout = this.workspaceManager.getLayout(workspace); diff --git a/lib/monitor.js b/lib/monitor.js index cf6f5be..65a717f 100644 --- a/lib/monitor.js +++ b/lib/monitor.js @@ -193,7 +193,9 @@ export class MonitorManager { this.controller.setBatchMode(true); const windows = workspace.list_windows(); windows.forEach(w => { - if (w.get_monitor() === monitorIndex && (!w.minimized || includeMinimized)) { + const wrapper = this.controller._windowWrappers.get(w); + const m = wrapper ? wrapper.effectiveMonitorIndex : w.get_monitor(); + if (m === monitorIndex && (!w.minimized || includeMinimized)) { w.delete(global.get_current_time()); } }); @@ -221,10 +223,13 @@ export class MonitorManager { this.controller.setBatchMode(true); const windows = workspace.list_windows(); windows.forEach(w => { - const m = w.get_monitor(); + const wrapper = this.controller._windowWrappers.get(w); + const m = wrapper ? wrapper.effectiveMonitorIndex : w.get_monitor(); if (m === activeMonitorIndex) { + if (wrapper) wrapper._expectedMonitorIndex = targetMonitorIndex; w.move_to_monitor(targetMonitorIndex); } else if (m === targetMonitorIndex) { + if (wrapper) wrapper._expectedMonitorIndex = activeMonitorIndex; w.move_to_monitor(activeMonitorIndex); } }); @@ -251,7 +256,10 @@ export class MonitorManager { this.controller.setBatchMode(true); const windows = activeWorkspace.list_windows(); windows.forEach(w => { - if (w.get_monitor() === monitorIndex) { + const wrapper = this.controller._windowWrappers.get(w); + const m = wrapper ? wrapper.effectiveMonitorIndex : w.get_monitor(); + if (m === monitorIndex) { + if (wrapper) wrapper._expectedWorkspace = targetWorkspace; w.change_workspace(targetWorkspace); } }); diff --git a/lib/window.js b/lib/window.js index 36b84a2..f9b900d 100644 --- a/lib/window.js +++ b/lib/window.js @@ -29,6 +29,32 @@ export class WindowWrapper { return (this.window.get_title && this.window.get_title()) || 'Unknown'; } + // Expected State Cache to prevent race condition when working with monitors in rapid succession + get effectiveMonitorIndex() { + let m = this.window.get_monitor ? this.window.get_monitor() : -1; + if (this._expectedMonitorIndex !== undefined) { + if (m !== this._expectedMonitorIndex) { + m = this._expectedMonitorIndex; + } else { + delete this._expectedMonitorIndex; + } + } + return m; + } + + // Expected State Cache to prevent race condition when working with workspaces in rapid succession + get effectiveWorkspace() { + let w = this.window.get_workspace ? this.window.get_workspace() : null; + if (this._expectedWorkspace !== undefined) { + if (w !== this._expectedWorkspace) { + w = this._expectedWorkspace; + } else { + delete this._expectedWorkspace; + } + } + return w; + } + bindSignals() { if (!this.signals.has('unmanaged')) { this.signals.set('unmanaged', this.window.connect('unmanaged', () => this.controller.untile(this.window))); From 1d4f8342a3646d85e761592bc7dac7fa62f33482 Mon Sep 17 00:00:00 2001 From: Konstantin Merkel Date: Wed, 10 Jun 2026 17:51:41 +0200 Subject: [PATCH 2/2] bugfix: prevent infinite Wayland loop during Expected State Cache resolution --- lib/window.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/window.js b/lib/window.js index f9b900d..e4257a3 100644 --- a/lib/window.js +++ b/lib/window.js @@ -116,6 +116,11 @@ export class WindowWrapper { applyGeometry(rect) { if (this.unmanaged || !this.window.move_resize_frame || this.window.minimized) return; + // Prevent infinite resize loops caused by Wayland clamping premature out-of-bounds resizes. + // Wait for Mutter's asynchronous state to catch up to our Expected State Cache. + if (this._expectedMonitorIndex !== undefined && this.window.get_monitor() !== this._expectedMonitorIndex) return; + if (this._expectedWorkspace !== undefined && this.window.get_workspace() !== this._expectedWorkspace) return; + try { if (this.window.get_monitor && this.monitorIndex >= 0 && this.window.get_monitor() !== this.monitorIndex) { if (this.window.move_to_monitor) {