Skip to content

XWayland windows are never released: destroyed and unmapped X windows stay in the window list and keep focus #53

Description

@tomocrafter

Summary

ShojiWM keeps XWayland toplevels in its managed window list forever. Windows are retained both when the X window is merely unmapped and when the client process exits entirely. COMPOSITOR.event.onClose never fires for them, so config-side cleanup (HYBRID_WINDOW_MANAGER.onClose in the default config) is never invoked.

The retained windows also keep isFocused === true, so multiple windows report focus simultaneously. This corrupts the window stack and focus handling for all windows, including native Wayland ones.

Environment

ShojiWM 387436b9e3ed (source install via dist/install.sh)
xwayland-satellite 0.8.2
xorg-xwayland 24.1.13
wayland / mesa 1.25.0 / 26.1.6
Kernel 7.1.4 (Arch, MacBookPro16,2 / T2)
GPU Intel Iris Plus Graphics G7 (ICL GT2), i915
rustc 1.97.1
Output eDP-1, 2560x1600, scale: 1.8

Reproduction

  1. Start a ShojiWM session.
  2. Launch Steam (any app that creates many short-lived X windows works; Steam is convenient because it creates a login dialog, notification toasts, dropdown menus and a Shutdown window).
  3. Quit Steam completely and confirm no processes remain: pgrep -c steam0.
  4. Query the compositor over the config's IPC socket:
import json, os, socket
p = f"{os.environ['XDG_RUNTIME_DIR']}/shojiwm-{os.environ['WAYLAND_DISPLAY']}.sock"
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.connect(p)
s.sendall(b'{"id":1,"method":"workspaces.get"}\n')
buf = b""
while b"\n" not in buf: buf += s.recv(65536)
print(json.dumps(json.loads(buf.decode().splitlines()[0])["result"], indent=2))

Observed

With zero Steam processes alive, the compositor still reports 14 windows, 13 of them steam, and four of them focused at once:

total: 14   focused=true: 4
  0x0   com.mitchellh.ghostty  focused=True   (the only real window)
  0xc   steam                  focused=True   Steam
  0x11  steam                  focused=False
  0x12  steam                  focused=False
  0x13  steam                  focused=True
  0x14 … 0x1b  steam           focused=False
  0x1c  steam                  focused=True   Shutdown

Cross-checking the X server at the same moment — only one window is actually viewable:

IsViewable:  1
IsUnMapped: 19
_NET_CLIENT_LIST entries: 1

So the compositor holds 14 windows where X has 1 mapped toplevel.

The same retention happens while the client is alive: Steam keeps its login dialog and toasts around unmapped, and those are counted as managed, focusable windows too.

Expected

  • An XWayland window that is unmapped or whose client has exited should be removed from the managed window list, and COMPOSITOR.event.onClose should fire for it.
  • At most one window should have isFocused === true.

Impact

Everything below traces back to this single cause:

  • Focus and stacking break for unrelated windows. With several windows reporting focus, Workspace.focusedWindow() in the default config (this.windows.find(w => read(w.isFocused))) returns the first match, which is usually a dead XWayland window. A newly focused window is not raised, so a stale window stays visually on top. This affects native Wayland clients too — e.g. focusing Brave leaves a Ghostty window rendered above it.
  • Alt+Tab / window switchers are unusable, since the window list is mostly invisible entries.
  • Steam is effectively unusable. Its dropdown menus, tooltips and notification toasts each become decorated toplevels. They also accumulate, so the list grows on every interaction.
  • Ghost windows can only be cleared by restarting the compositor; a config hot reload does not drop them.

Notes

  • The default config's wiring looks correct — COMPOSITOR.event.onClose((window) => HYBRID_WINDOW_MANAGER.onClose(window)) is present and onClose removes the window from windowStack and every workspace. The event simply never arrives for these windows, which points at the compositor/xwayland-satellite integration rather than the config.
  • Steam does not set a useful _NET_WM_WINDOW_TYPE on its menus/toasts (they report _NET_WM_WINDOW_TYPE_NORMAL, and the toasts are override-redirect and absent from _NET_CLIENT_LIST), so type hints alone are not enough to filter them — the unmap/destroy path is what needs fixing.
  • Possibly related, but filed separately if you'd prefer: under scale: 1.8, Steam creates its main window at 2512x1484 while the XWayland screen is 1422x889 (the logical size), making it larger than the display and offsetting input coordinates. STEAM_FORCE_DESKTOPUI_SCALING=1 and Xft.dpi: 96 have no effect. xrandr reports the output as 0mm x 0mm.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions