Repository: qtile/qtile

Issue:
[Wayland] Crash on client_killed hook when calling cmd_to_layout_index
### The issue:

Hi,

I've stumbled upon an issue that appears on Wayland. I'm using Qtile version 0.21.1.dev9+g634a470a.

My use case is that I want to automatically fall back to a "default" layout whenever there is only a single window left on the given group.

To achieve this I utilize the client_killed hook:
```python
@hook.subscribe.client_killed
def fallback_default_layout(client: Window) -> None:
    """Reset a group to default layout when theres is only one window left"""
    assert qtile is not None, "This should never be None"

    if client.group is None:
        return

    try:
        win_count = len(client.group.windows)
    except AttributeError:
        win_count = 0

    if win_count > 2:
        return

    group_name: str = client.group.name
    default_layout_index: int = 0

    qtile.cmd_to_layout_index(default_layout_index, group_name)
```

The above works fine on X11, but I just now started testing out Wayland and noticed that this unfortunately causes Qtile to silently crash when triggered.
I managed to verify (using logger) that the culprit indeed seems to be the cmd_to_layout_index function on the last row.

I'm not getting anything out of the logs when the crash happens though, despite also trying the debug level. If you have any pointers on how I could go about and dig into this further I'm happy to do so.

Thank you for developing Qtile!

### Required:

- [X] I have searched past issues to see if this bug has already been reported.


Discussion:
I did some more digging today and think I have further narrowed it down. 
The crash occurs when [`layout_all`](https://github.com/qtile/qtile/blob/ea531114fba129096a5cf1efebf8e6883f62725d/libqtile/group.py#L145-L168) tries to layout the windows, specifically when it loops each window in the group and calls [`configure`](https://github.com/qtile/qtile/blob/master/libqtile/layout/base.py#L53=L56) (if that makes sense and I understood it correctly).

Still not entirely sure what's going on and why this only crashes in Wayland, but hopefully this might help.
 
This change should be sufficient to fix it:
```diff
diff --git a/libqtile/core/manager.py b/libqtile/core/manager.py
index 39467055..193b0e35 100644
--- a/libqtile/core/manager.py
+++ b/libqtile/core/manager.py
@@ -628,7 +628,6 @@ class Qtile(CommandObject):
     def unmanage(self, wid: int) -> None:
         c = self.windows_map.get(wid)
         if c:
-            hook.fire("client_killed", c)
             if isinstance(c, base.Static):
                 if c.reserved_space:
                     self.free_reserved_space(c.reserved_space, c.screen)
@@ -637,6 +636,7 @@ class Qtile(CommandObject):
                     c.group.remove(c)
             del self.windows_map[wid]
             self.core.update_client_list(self.windows_map)
+            hook.fire("client_killed", c)
 
     def find_screen(self, x: int, y: int) -> Screen | None:
         """Find a screen based on the x and y offset"""

```
However I don't understand why it isn't like this already. Why would the client_killed hook be fired before removing that client from the group? Searching finds this commit: https://github.com/qtile/qtile/commit/944809db8871aaa4c98d1cfacaf44315e354177d. For this reason I'm a bit worried that this will have unintended consequences. I think it should be fine though.

Please could you test out qtile with that change for a while and then report back how it goes, if it introduces any issues, fixes the issue etc?

Resolving patch:
diff --git a/libqtile/core/manager.py b/libqtile/core/manager.py
--- a/libqtile/core/manager.py
+++ b/libqtile/core/manager.py
@@ -658,15 +658,20 @@ def manage(self, win: base.WindowType) -> None:
     def unmanage(self, wid: int) -> None:
         c = self.windows_map.get(wid)
         if c:
-            hook.fire("client_killed", c)
+            group = None
             if isinstance(c, base.Static):
                 if c.reserved_space:
                     self.free_reserved_space(c.reserved_space, c.screen)
             elif isinstance(c, base.Window):
                 if c.group:
+                    group = c.group
                     c.group.remove(c)
             del self.windows_map[wid]
             self.core.update_client_list(self.windows_map)
+            if isinstance(c, base.Window):
+                # Put the group back on the window so hooked functions can access it.
+                c.group = group
+            hook.fire("client_killed", c)
 
     def find_screen(self, x: int, y: int) -> Screen | None:
         """Find a screen based on the x and y offset"""
diff --git a/libqtile/widget/window_count.py b/libqtile/widget/window_count.py
--- a/libqtile/widget/window_count.py
+++ b/libqtile/widget/window_count.py
@@ -70,8 +70,6 @@ def _wincount(self, *args):
     def _win_killed(self, window):
         try:
             self._count = len(self.bar.screen.group.windows)
-            if window.group == self.bar.screen.group:
-                self._count -= 1
         except AttributeError:
             self._count = 0
 


Test patch: