fix(extension): WindowGuard skips desktop-icons-ng; harden warden ordering
P2 went onto the VM and the dashboard workspace appeared, but switching to it auto-bounced back to the previous workspace. Diagnostic build showed every `window-added` on dashboard was firing for a window with title `@!0,0;BDHF`, wm_class=gjs, type=NORMAL, sticky=false. That's the desktop-icons-ng (ding) extension's per-monitor desktop window: it runs as a separate gjs process, so it masquerades as a generic NORMAL window and the original DESKTOP/DOCK/SPLASHSCREEN filter missed it. WindowGuard was un-sticking it on every workspace switch, which caused mutter to flip the active workspace away from dashboard. WindowGuard.shouldSkip now also returns true for `is_skip_taskbar()` windows. That's the load-bearing check for ding, and a sensible general rule: skip-taskbar windows are owner-managed (PiP, helper utilities, backgrounded ding desktops) and shouldn't be dragged around by us. The DESKTOP/DOCK/SPLASHSCREEN check stays for extensions that do label themselves correctly. While re-deploying, also harden the WorkspaceWarden enable() ordering. The original sequence relied on mutter not reacting to the `dynamic-workspaces=false` flip before our `append_new_workspace` call landed -- but with default gsettings (num-workspaces=4), a faster reaction would expand the count to 4 and leave us with ghost workspaces. Reorder to: write num-workspaces=current first, then flip dynamic, then append, then update num-workspaces. The race is now closed regardless of mutter timing. Verified on ubuntu2504-test VM: dashboard workspace switchable, ding silently ignored, real-window bounces still log on Super+Shift+End. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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=<current count> 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());
|
||||
|
||||
Reference in New Issue
Block a user