diff --git a/extension/src/window-guard.ts b/extension/src/window-guard.ts index 3bdf9e4..53fd3e5 100644 --- a/extension/src/window-guard.ts +++ b/extension/src/window-guard.ts @@ -1,4 +1,4 @@ -import type Meta from 'gi://Meta'; +import Meta from 'gi://Meta'; import { log, warn } from './util/logger.js'; import type { WorkspaceWarden } from './workspace-warden.js'; @@ -101,6 +101,8 @@ export class WindowGuard { private trackWindow(win: Meta.Window): void { if (this.trackedWindows.has(win)) return; if (win.is_override_redirect()) return; + // Don't bother tracking windows we'd never bounce. + if (this.shouldSkip(win)) return; // Record current non-dashboard workspace if any. const ws = win.get_workspace(); @@ -140,21 +142,46 @@ export class WindowGuard { if (this.disposed) return; if (this.whitelist.has(win)) return; if (win.is_override_redirect()) return; + if (this.shouldSkip(win)) return; // Make sure we're tracking it (covers windows mapped directly onto // dashboard before workspace-changed could fire). if (!this.trackedWindows.has(win)) this.trackWindow(win); + const title = win.get_title?.() ?? '?'; const fallback = this.resolveFallback(win); if (!fallback) { - warn(`bounceIfNeeded: no fallback workspace found for ${win.get_title?.() ?? '?'}`); + warn(`bounceIfNeeded: no fallback workspace for "${title}"`); return; } - log(`bouncing "${win.get_title?.() ?? '?'}" from dashboard to ws ${fallback.index()}`); + const wmClass = win.get_wm_class?.() ?? '?'; + log(`bouncing "${title}" cls=${wmClass} type=${win.get_window_type()} → ws ${fallback.index()}`); win.change_workspace(fallback); } + // Windows we never bounce: anything the user can't normally reach via + // the taskbar / dock (ding's @!0,0;BDHF desktop window, PiP popups, + // helper utility windows), windows pinned across all workspaces, and + // windows that explicitly declare themselves as desktop / dock / + // splash. Moving these triggers surprising side effects — un-sticking + // ding's desktop window in particular causes the active workspace to + // bounce, which is what manifested in the first VM test. + // + // NB ding masquerades as a generic GJS NORMAL window (wm_class=gjs, + // type=NORMAL, sticky=false), so DESKTOP/DOCK type matching alone + // misses it -- the load-bearing check here is is_skip_taskbar(). + private shouldSkip(win: Meta.Window): boolean { + if (win.is_on_all_workspaces()) return true; + if (win.is_skip_taskbar()) return true; + const t = win.get_window_type(); + return ( + t === Meta.WindowType.DESKTOP || + t === Meta.WindowType.DOCK || + t === Meta.WindowType.SPLASHSCREEN + ); + } + private resolveFallback(win: Meta.Window): Meta.Workspace | null { const wm = global.workspace_manager; const dashIdx = this.warden.getDashboardIndex(); diff --git a/extension/src/workspace-warden.ts b/extension/src/workspace-warden.ts index 41f6f97..c1e2033 100644 --- a/extension/src/workspace-warden.ts +++ b/extension/src/workspace-warden.ts @@ -98,11 +98,15 @@ export class WorkspaceWarden { }; saveState(STATE_NAME, this.saved); - // Order matters: turn off dynamic FIRST so mutter doesn't react to our - // append by GC'ing it back. Setting dynamic=false makes the count - // authoritative from num-workspaces; we'll update num-workspaces to - // match after the append. + // Order matters. The defaults on a fresh GNOME session are + // dynamic-workspaces=true and num-workspaces=4 (the latter is unused + // while dynamic mode is on). If we flip dynamic=false first, mutter + // will react to the stale num-workspaces=4 and expand the count to 4 + // (creating up to N+1 ghost workspaces). Avoid that race by writing + // num-workspaces= first, then flipping dynamic, then + // appending the dashboard, then updating num-workspaces to the new count. this.suppressGuard = true; + this.wmSettings.set_int(KEY_NUM, n); this.mutterSettings.set_boolean(KEY_DYNAMIC, false); this.dashboardWs = wm.append_new_workspace(false, global.get_current_time());