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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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