Repository: qtile/qtile

Issue:
No icon for some apps in Tasklist widget
### The issue:

```console
$ qtile --version
    0.21.1.dev67+g2b2cd60b
```

As far as I've checked, this only happens with apps that don't "publish" an X property for icon (_NET_WM_ICON, WM_ICON_NAME). Can the WM_CLASS property be used for this, possibly?

`I know what I have to do, but I don't know if I have the strength to do it.`

![image](https://user-images.githubusercontent.com/84532402/174458652-bcb2245d-3aa6-4fd8-a11f-499cb269ddb2.png)
`That little smudge of color is supposed to be the icon :)`

![image](https://user-images.githubusercontent.com/84532402/174458680-3785b5b2-9f4a-49a4-92c5-a124f05cc03e.png)
`Normal behaviour`


```console
$ xprop
XdndProxy(WINDOW): window id # 0x4600004
WM_CLASS(STRING) = "spotify", "Spotify"
WM_NAME(STRING) = "Spotify"
_NET_WM_NAME(UTF8_STRING) = "Spotify"
_NET_WM_STATE(ATOM) = _NET_WM_STATE_FOCUSED
_NET_FRAME_EXTENTS(CARDINAL) = 0, 0, 0, 0
_NET_WM_DESKTOP(CARDINAL) = 5
WM_STATE(WM_STATE):
		window state: Normal
		icon window: 0x0
WM_NORMAL_HINTS(WM_SIZE_HINTS):
		program specified location: 0, 0
_NET_WM_PID(CARDINAL) = 34927
WM_CLIENT_MACHINE(STRING) = "arch"
WM_PROTOCOLS(ATOM): protocols  WM_DELETE_WINDOW, _NET_WM_PING
```

```python
widget.TaskList(
    parse_text=no_text,
    highlight_method="block",
    icon_size=48,
    border=colors[0],
    rounded=True,
    padding=0,
    padding_y=-3,
    margin_x=2,
    margin_y=45,
    txt_floating="🗗 ",
    txt_maximized="🗖 ",
    txt_minimized="🗕 "
),
```

### Required:

- [X] I have searched past issues to see if this bug has already been reported.


Discussion:
What we should be doing is looking up icons installed in the user's system (using a library like `xdg`). This would also allow us to have icons on Wayland.

I can take a look at adding this.

Resolving patch:
diff --git a/libqtile/widget/tasklist.py b/libqtile/widget/tasklist.py
--- a/libqtile/widget/tasklist.py
+++ b/libqtile/widget/tasklist.py
@@ -27,7 +27,15 @@
 
 import cairocffi
 
+try:
+    from xdg.IconTheme import getIconPath
+
+    has_xdg = True
+except ImportError:
+    has_xdg = False
+
 from libqtile import bar, hook, pangocffi
+from libqtile.images import Img
 from libqtile.log_utils import logger
 from libqtile.widget import base
 
@@ -37,6 +45,9 @@ class TaskList(base._Widget, base.PaddingMixin, base.MarginMixin):
 
     Contrary to WindowTabs this is an interactive widget.  The window that
     currently has focus is highlighted.
+
+    Optional requirements: `pyxdg <https://pypi.org/project/pyxdg/>`__ is needed
+    to use theme icons and to display icons on Wayland.
     """
 
     orientations = base.ORIENTATION_HORIZONTAL
@@ -141,6 +152,18 @@ class TaskList(base._Widget, base.PaddingMixin, base.MarginMixin):
             None,
             "Icon size. " "(Calculated if set to None. Icons are hidden if set to 0.)",
         ),
+        (
+            "theme_mode",
+            None,
+            "When to use theme icons. `None` = never, `preferred` = use if available, "
+            "`fallback` = use if app does not provide icon directly. "
+            "`preferred` and `fallback` have identical behaviour on Wayland.",
+        ),
+        (
+            "theme_path",
+            None,
+            "Path to icon theme to be used by pyxdg for icons. ``None`` will use default icon theme.",
+        ),
     ]
 
     def __init__(self, **config):
@@ -288,10 +311,19 @@ def calc_box_widths(self):
     def _configure(self, qtile, bar):
         base._Widget._configure(self, qtile, bar)
 
-        if qtile.core.name == "wayland" and self.icon_size != 0:
+        if not has_xdg and self.theme_mode is not None:
+            logger.warning("You must install pyxdg to use theme icons.")
+            self.theme_mode = None
+
+        if self.theme_mode and self.theme_mode not in ["preferred", "fallback"]:
+            logger.warning(
+                "Unexpected theme_mode (%s). Theme icons will be disabled.", self.theme_mode
+            )
+            self.theme_mode = None
+
+        if qtile.core.name == "wayland" and self.theme_mode is None and self.icon_size != 0:
             # Disable icons
             self.icon_size = 0
-            logger.warning("TaskList icons not supported in Wayland.")
 
         if self.icon_size is None:
             self.icon_size = self.bar.height - 2 * (self.borderwidth + self.margin_y)
@@ -398,14 +430,10 @@ def select_window(self):
             else:
                 window.cmd_toggle_minimize()
 
-    def get_window_icon(self, window):
-        if not window.icons:
+    def _get_class_icon(self, window):
+        if not getattr(window, "icons", False):
             return None
 
-        cache = self._icons_cache.get(window.wid)
-        if cache:
-            return cache
-
         icons = sorted(
             iter(window.icons.items()),
             key=lambda x: abs(self.icon_size - int(x[0].split("x")[0])),
@@ -417,16 +445,63 @@ def get_window_icon(self, window):
             icon[1], cairocffi.FORMAT_ARGB32, width, height
         )
 
-        surface = cairocffi.SurfacePattern(img)
+        return img
+
+    def _get_theme_icon(self, window):
+        classes = window.get_wm_class()
+
+        if not classes:
+            return None
+
+        icon = None
+
+        for cl in classes:
+            for app in set([cl, cl.lower()]):
+                icon = getIconPath(app, theme=self.theme_path)
+                if icon is not None:
+                    break
+            else:
+                continue
+            break
+
+        if not icon:
+            return None
+
+        img = Img.from_path(icon)
+
+        return img.surface
+
+    def get_window_icon(self, window):
+        if not getattr(window, "icons", False) and self.theme_mode is None:
+            return None
+
+        cache = self._icons_cache.get(window.wid)
+        if cache:
+            return cache
 
-        scaler = cairocffi.Matrix()
+        surface = None
+        img = None
+
+        if self.qtile.core.name == "x11":
+            img = self._get_class_icon(window)
+
+        if self.theme_mode == "preferred" or (self.theme_mode == "fallback" and img is None):
+            xdg_img = self._get_theme_icon(window)
+            if xdg_img:
+                img = xdg_img
+
+        if img is not None:
+            surface = cairocffi.SurfacePattern(img)
+            height = img.get_height()
+            width = img.get_width()
+            scaler = cairocffi.Matrix()
+            if height != self.icon_size:
+                sp = height / self.icon_size
+                height = self.icon_size
+                width /= sp
+                scaler.scale(sp, sp)
+            surface.set_matrix(scaler)
 
-        if height != self.icon_size:
-            sp = height / self.icon_size
-            height = self.icon_size
-            width /= sp
-            scaler.scale(sp, sp)
-        surface.set_matrix(scaler)
         self._icons_cache[window.wid] = surface
         return surface
 


Test patch: