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
- Start a ShojiWM session.
- 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).
- Quit Steam completely and confirm no processes remain:
pgrep -c steam → 0.
- 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.
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.onClosenever fires for them, so config-side cleanup (HYBRID_WINDOW_MANAGER.onClosein 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
387436b9e3ed(source install viadist/install.sh)scale: 1.8Reproduction
Shutdownwindow).pgrep -c steam→0.Observed
With zero Steam processes alive, the compositor still reports 14 windows, 13 of them
steam, and four of them focused at once:Cross-checking the X server at the same moment — only one window is actually viewable:
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
COMPOSITOR.event.onCloseshould fire for it.isFocused === true.Impact
Everything below traces back to this single cause:
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.Notes
COMPOSITOR.event.onClose((window) => HYBRID_WINDOW_MANAGER.onClose(window))is present andonCloseremoves the window fromwindowStackand every workspace. The event simply never arrives for these windows, which points at the compositor/xwayland-satellite integration rather than the config._NET_WM_WINDOW_TYPEon 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.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=1andXft.dpi: 96have no effect.xrandrreports the output as0mm x 0mm.