Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package qtile for openSUSE:Factory checked 
in at 2024-08-16 12:23:59
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/qtile (Old)
 and      /work/SRC/openSUSE:Factory/.qtile.new.2698 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "qtile"

Fri Aug 16 12:23:59 2024 rev:30 rq:1194150 version:0.28.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/qtile/qtile.changes      2024-07-22 
17:18:23.914223266 +0200
+++ /work/SRC/openSUSE:Factory/.qtile.new.2698/qtile.changes    2024-08-16 
12:24:33.777308660 +0200
@@ -1,0 +2,9 @@
+Thu Aug 15 13:45:50 UTC 2024 - Soc Virnyl Estela <o...@uncomfyhalomacro.pl>
+
+- Update to version 0.28.1:
+  * bugfixes
+    - fix a crash in the StatusNotifier widget #4959 #4960
+    - various bug fixes to widgets from previous releases
+    - fix xrandr commands racing with qtile startup
+
+-------------------------------------------------------------------

Old:
----
  qtile-0.27.0.tar.gz

New:
----
  qtile-0.28.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ qtile.spec ++++++
--- /var/tmp/diff_new_pack.N7MGlp/_old  2024-08-16 12:24:34.225327279 +0200
+++ /var/tmp/diff_new_pack.N7MGlp/_new  2024-08-16 12:24:34.229327445 +0200
@@ -20,7 +20,7 @@
 %global _conflict_wlroots_ver 0.18.0
 
 Name:           qtile
-Version:        0.27.0
+Version:        0.28.1
 Release:        0
 Summary:        A pure-Python tiling window manager
 # All MIT except for: libqtile/widget/pacman.py:GPL (v3 or later)

++++++ qtile-0.27.0.tar.gz -> qtile-0.28.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/CHANGELOG new/qtile-0.28.1/CHANGELOG
--- old/qtile-0.27.0/CHANGELOG  2024-07-13 00:08:56.000000000 +0200
+++ new/qtile-0.28.1/CHANGELOG  2024-08-13 01:39:10.000000000 +0200
@@ -2,6 +2,15 @@
     * features
     * bugfixes
 
+Qtile 0.28.1, released 2024-08-12:
+    * bugfixes
+        - fix a crash in the StatusNotifier widget #4959 #4960
+
+Qtile 0.28.0, released 2024-08-11:
+    * bugfixes
+        - various bug fixes to widgets from previous releases
+        - fix xrandr commands racing with qtile startup
+
 Qtile 0.27.0, released 2024-07-12:
     * features
         - Make default `Plasma` add mode dynamic
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/PKG-INFO new/qtile-0.28.1/PKG-INFO
--- old/qtile-0.27.0/PKG-INFO   2024-07-13 00:09:04.178852000 +0200
+++ new/qtile-0.28.1/PKG-INFO   2024-08-13 01:39:19.010635100 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: qtile
-Version: 0.27.0
+Version: 0.28.1
 Summary: A pure-Python tiling window manager.
 License: MIT
 Project-URL: Homepage, https://qtile.org
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/libqtile/backend/x11/core.py 
new/qtile-0.28.1/libqtile/backend/x11/core.py
--- old/qtile-0.27.0/libqtile/backend/x11/core.py       2024-07-13 
00:08:56.000000000 +0200
+++ new/qtile-0.28.1/libqtile/backend/x11/core.py       2024-08-13 
01:39:10.000000000 +0200
@@ -185,7 +185,10 @@
         self.conn.finalize()
 
     def get_screen_info(self) -> list[ScreenRect]:
-        return self.conn.pseudoscreens
+        ps = self.conn.pseudoscreens
+        if self.qtile:
+            self._xpoll()
+        return ps
 
     @property
     def wmname(self):
@@ -271,7 +274,6 @@
 
             self.update_client_lists()
             win.change_layer()
-            self.conn.enable_screen_change_notifications()
 
     def warp_pointer(self, x, y):
         self._root.warp_pointer(x, y)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/libqtile/backend/x11/xcbq.py 
new/qtile-0.28.1/libqtile/backend/x11/xcbq.py
--- old/qtile-0.27.0/libqtile/backend/x11/xcbq.py       2024-07-13 
00:08:56.000000000 +0200
+++ new/qtile-0.28.1/libqtile/backend/x11/xcbq.py       2024-08-13 
01:39:10.000000000 +0200
@@ -402,6 +402,7 @@
 class RandR:
     def __init__(self, conn):
         self.ext = conn.conn(xcffib.randr.key)
+        self.ext.SelectInput(conn.default_screen.root.wid, 
xcffib.randr.NotifyMask.ScreenChange)
 
     def query_crtcs(self, root):
         infos = []
@@ -411,9 +412,6 @@
             infos.append(ScreenRect(crtc_info.x, crtc_info.y, crtc_info.width, 
crtc_info.height))
         return infos
 
-    def enable_screen_change_notifications(self, conn):
-        self.ext.SelectInput(conn.default_screen.root.wid, 
xcffib.randr.NotifyMask.ScreenChange)
-
 
 class XFixes:
     selection_mask = (
@@ -497,9 +495,6 @@
         elif hasattr(self, "randr"):
             return self.randr.query_crtcs(self.screens[0].root.wid)
 
-    def enable_screen_change_notifications(self):
-        self.randr.enable_screen_change_notifications(self)
-
     def finalize(self):
         self.cursors.finalize()
         self.disconnect()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/libqtile/dgroups.py 
new/qtile-0.28.1/libqtile/dgroups.py
--- old/qtile-0.27.0/libqtile/dgroups.py        2024-07-13 00:08:56.000000000 
+0200
+++ new/qtile-0.28.1/libqtile/dgroups.py        2024-08-13 01:39:10.000000000 
+0200
@@ -253,7 +253,7 @@
                 self.sort_groups()
             del self.timeout[client]
 
-        if group.persist:
+        if group is not None and group.persist:
             return
 
         logger.debug("Deleting %s in %ss", group, self.delay)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/libqtile/hook.py 
new/qtile-0.28.1/libqtile/hook.py
--- old/qtile-0.27.0/libqtile/hook.py   2024-07-13 00:08:56.000000000 +0200
+++ new/qtile-0.28.1/libqtile/hook.py   2024-08-13 01:39:10.000000000 +0200
@@ -35,7 +35,7 @@
 import contextlib
 from typing import TYPE_CHECKING
 
-from libqtile import utils
+from libqtile import backend, utils
 from libqtile.log_utils import logger
 from libqtile.resources.sleep import inhibitor
 
@@ -175,6 +175,9 @@
     def fire(self, event, *args, **kwargs):
         if event not in self.subscribe.hooks:
             raise utils.QtileError("Unknown event: %s" % event)
+        # Do not fire for Internal windows
+        if any(isinstance(arg, backend.base.window.Internal) for arg in args):
+            return
         # We should check if the registry name is in the subscriptions dict
         # A name can disappear if the config is reloaded (which clears 
subscriptions)
         # but there are no hook subscriptions. This is not an issue for qtile 
core but
@@ -569,7 +572,8 @@
     Hook(
         "client_killed",
         """
-        Called after a client has been unmanaged
+        Called after a client has been unmanaged. This hook is not called for
+        internal windows.
 
         **Arguments**
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/libqtile/scratchpad.py 
new/qtile-0.28.1/libqtile/scratchpad.py
--- old/qtile-0.27.0/libqtile/scratchpad.py     2024-07-13 00:08:56.000000000 
+0200
+++ new/qtile-0.28.1/libqtile/scratchpad.py     2024-08-13 01:39:10.000000000 
+0200
@@ -179,7 +179,7 @@
             net_wm_state = list(window.window.get_property("_NET_WM_STATE", 
"ATOM", unpack=int))
             skip_taskbar = 
window.qtile.core.conn.atoms["_NET_WM_STATE_SKIP_TASKBAR"]
             if net_wm_state:
-                if "_NET_WM_STATE_SKIP_TASKBAR" not in net_wm_state:
+                if skip_taskbar not in net_wm_state:
                     net_wm_state.append(skip_taskbar)
             else:
                 net_wm_state = [skip_taskbar]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/libqtile/scripts/check.py 
new/qtile-0.28.1/libqtile/scripts/check.py
--- old/qtile-0.27.0/libqtile/scripts/check.py  2024-07-13 00:08:56.000000000 
+0200
+++ new/qtile-0.28.1/libqtile/scripts/check.py  2024-08-13 01:39:10.000000000 
+0200
@@ -151,12 +151,12 @@
             except CheckError:
                 valid = False
 
-    if valid:
-        print("Your config can be loaded by Qtile.")
-    else:
-        print(
-            "Your config is valid python but has type checking errors. This 
may result in unexpected behaviour."
-        )
+        if valid:
+            print("Your config can be loaded by Qtile.")
+        else:
+            print(
+                "Your config is valid python but has type checking errors. 
This may result in unexpected behaviour."
+            )
 
 
 def add_subcommand(subparsers, parents):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/libqtile/widget/cmus.py 
new/qtile-0.28.1/libqtile/widget/cmus.py
--- old/qtile-0.27.0/libqtile/widget/cmus.py    2024-07-13 00:08:56.000000000 
+0200
+++ new/qtile-0.28.1/libqtile/widget/cmus.py    2024-08-13 01:39:10.000000000 
+0200
@@ -14,6 +14,8 @@
 # You should have received a copy of the GNU General Public License
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
+import datetime
+import math
 import subprocess
 from functools import partial
 
@@ -21,6 +23,11 @@
 from libqtile.widget import base
 
 
+def format_time(time_seconds_string):
+    """Format time in seconds as [h:]mm:ss."""
+    return 
str(datetime.timedelta(seconds=float(time_seconds_string))).lstrip("0").lstrip(":")
+
+
 class Cmus(base.ThreadPoolText):
     """A simple Cmus widget.
 
@@ -31,15 +38,55 @@
         - skip forward in playlist on scroll up;
         - skip backward in playlist on scroll down.
 
+    The following fields (extracted from ``cmus-remote -C status``) are 
available in the `format`
+    string:
+
+    - ``status``: cmus playback status, one of "playing", "paused" or 
"stopped".
+    - ``file``
+    - ``position``: Current position in [h:]mm:ss.
+    - ``position_percent``: Current position in percent.
+    - ``remaining``: Remaining time in [h:]mm:ss.
+    - ``remaining_percent``: Remaining time in percent.
+    - ``duration``: Total length in [h:]mm:ss.
+    - ``artist``
+    - ``album``
+    - ``albumartist``
+    - ``composer``
+    - ``comment``
+    - ``date``
+    - ``discnumber``
+    - ``genre``
+    - ``title``: Title or filename if no title is available.
+    - ``tracknumber``
+    - ``stream``
+    - ``status_text``: Text indicating the playback status, corresponds to one 
of `playing_text`,
+      `paused_text` or `stopped_text`.
+
     Cmus (https://cmus.github.io) should be installed.
     """
 
     defaults = [
-        ("format", "{play_icon}{artist} - {title}", "Format of playback 
info."),
-        ("play_icon", "♫ ", "Icon to display, if chosen."),
-        ("play_color", "00ff00", "Text colour when playing."),
-        ("noplay_color", "cecece", "Text colour when not playing."),
+        ("format", "{status_text}{artist} - {title}", "Format of playback 
info."),
+        ("stream_format", "{status_text}{stream}", "Format of playback info 
for streams."),
+        (
+            "no_artist_format",
+            "{status_text}{title}",
+            "Format of playback info if no artist available.",
+        ),
+        ("playing_text", "♫ ", "Text to display when playing, if chosen."),
+        ("playing_color", "00ff00", "Text colour when playing."),
+        ("paused_text", "♫ ", "Text to display when paused, if chosen."),
+        ("paused_color", "cecece", "Text color when paused."),
+        ("stopped_text", "♫ ", "Text to display when stopped, if chosen."),
+        ("stopped_color", "cecece", "Text color when stopped."),
         ("update_interval", 0.5, "Update Time in seconds."),
+        (
+            "play_icon",
+            "♫ ",
+            "DEPRECATED Text to display when playing, paused, and stopped, if 
chosen.",
+        ),
+        ("play_color", "", "DEPRECATED Text colour when playing."),
+        ("noplay_color", "", "DEPRECATED Text colour when paused or stopped."),
     ]
 
     def __init__(self, **config):
@@ -56,6 +103,15 @@
             }
         )
 
+    def _configure(self, qtile, parent_bar):
+        base.ThreadPoolText._configure(self, qtile, parent_bar)
+        # Backwards compatibility
+        if self.play_color:
+            self.playing_color = self.play_color
+            self.paused_color = self.play_color
+        if self.noplay_color:
+            self.stopped_color = self.noplay_color
+
     def get_info(self):
         """Return a dictionary with info about the current cmus status."""
         try:
@@ -67,6 +123,11 @@
             info = {
                 "status": "",
                 "file": "",
+                "position": "",
+                "position_percent": "",
+                "remaining": "",
+                "remaining_percent": "",
+                "duration": "",
                 "artist": "",
                 "album": "",
                 "albumartist": "",
@@ -78,10 +139,13 @@
                 "title": "",
                 "tracknumber": "",
                 "stream": "",
+                "status_text": "",
                 "play_icon": self.play_icon,
             }
 
             for line in output:
+                if line.startswith("set"):
+                    break
                 for data in info:
                     match = data + " "
                     if match in line:
@@ -89,8 +153,33 @@
                         if index < 5:
                             info[data] = line[len(data) + index :].strip()
                             break
-                    elif line.startswith("set"):
-                        return info
+
+            # Set status text
+            status = info["status"]
+            info["status_text"] = getattr(self, f"{status}_text", 
self.stopped_text)
+
+            # Format and process duration and position
+            if info["position"] != "" and info["duration"] != "" and 
int(info["duration"]) > 0:
+                info["position_percent"] = (
+                    str(math.floor(int(info["position"]) / 
int(info["duration"]) * 100)) + "%"
+                )
+                info["remaining_percent"] = (
+                    str(
+                        math.ceil(
+                            (int(info["duration"]) - int(info["position"]))
+                            / int(info["duration"])
+                            * 100
+                        )
+                    )
+                    + "%"
+                )
+                info["remaining"] = format_time(int(info["duration"]) - 
int(info["position"]))
+                info["position"] = format_time(info["position"])
+                info["duration"] = format_time(info["duration"])
+            else:
+                info["duration"] = ""
+                info["position"] = ""
+
             return info
 
     def now_playing(self):
@@ -98,29 +187,31 @@
         info = self.get_info()
         now_playing = ""
         if info:
+            display_format = self.format
             status = info["status"]
             if self.status != status:
                 self.status = status
                 if self.status == "playing":
-                    self.layout.colour = self.play_color
+                    self.layout.colour = self.playing_color
+                elif self.status == "paused":
+                    self.layout.colour = self.paused_color
                 else:
-                    self.layout.colour = self.noplay_color
+                    self.layout.colour = self.stopped_color
             self.local = info["file"].startswith("/")
-            title = "{play_icon}" + info["title"]
             if self.local:
-                now_playing = self.format.format(**info)
-                if not info["artist"] and not info["title"]:
-                    file_path = info["file"]
-                    file_path = "{play_icon}" + file_path.split("/")[-1]
-                    now_playing = file_path.format(**info)
-            else:
-                if info["stream"]:
-                    stream_title = "{play_icon}" + info["stream"]
-                    now_playing = stream_title.format(**info)
-                else:
-                    now_playing = title.format(**info)
-        if now_playing == self.play_icon:
-            now_playing = ""
+                if not info["title"]:
+                    info["title"] = info["file"].split("/")[-1]
+                if not info["artist"]:
+                    display_format = self.no_artist_format
+            elif info["stream"]:
+                display_format = self.stream_format
+            # Handle case if cmus was started and no file is selected yet
+            elif not info["file"]:
+                display_format = ""
+            now_playing = display_format.format(**info)
+            if now_playing.strip() == info["status_text"].strip():
+                now_playing = ""
+
         return pangocffi.markup_escape_text(now_playing)
 
     def play(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/qtile-0.27.0/libqtile/widget/helpers/status_notifier/statusnotifier.py 
new/qtile-0.28.1/libqtile/widget/helpers/status_notifier/statusnotifier.py
--- old/qtile-0.27.0/libqtile/widget/helpers/status_notifier/statusnotifier.py  
2024-07-13 00:08:56.000000000 +0200
+++ new/qtile-0.28.1/libqtile/widget/helpers/status_notifier/statusnotifier.py  
2024-08-13 01:39:10.000000000 +0200
@@ -17,7 +17,7 @@
 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
-import os
+from contextlib import suppress
 from functools import partial
 from pathlib import Path
 
@@ -244,9 +244,11 @@
 
         try:
             icon_path = await self.item.get_icon_theme_path()
-            self.icon = self._get_custom_icon(icon_name, icon_path)
         except (AttributeError, DBusError):
-            pass
+            icon_path = None
+
+        if icon_path:
+            self.icon = self._get_custom_icon(icon_name, Path(icon_path))
 
         if not self.icon:
             self.icon = self._get_xdg_icon(icon_name)
@@ -275,10 +277,31 @@
         self._create_task_and_draw(self._get_icon("Overlay"))
 
     def _get_custom_icon(self, icon_name, icon_path):
+        icon = None
         for ext in [".png", ".svg"]:
-            path = os.path.join(icon_path, icon_name + ext)
-            if os.path.isfile(path):
-                return Img.from_path(path)
+            path = icon_path / f"{icon_name}{ext}"
+            if path.is_file():
+                icon = path
+                break
+
+        else:
+            # No icon found at the image path, let's search recursively
+            glob = icon_path.rglob(f"{icon_name}.*")
+            found = [icon for icon in glob if icon.is_file()]
+
+            # Found a matching icon in subfolder
+            if found:
+                # We'd prefer an svg file
+                svg = [icon for icon in found if icon.suffix.lower() == ".svg"]
+                if svg:
+                    icon = svg[0]
+                else:
+                    # If not, we'll take what there is
+                    # NOTE: not clear how we can handle multiple matches with 
different icon sizes 16x16, 32x32 etc
+                    icon = found[0]
+
+        if icon is not None:
+            return Img.from_path(icon.resolve().as_posix())
 
         return None
 
@@ -667,13 +690,16 @@
         self, on_item_added=None, on_item_removed=None, on_icon_changed=None
     ):
         if on_item_added is not None:
-            self._on_item_added.remove(on_item_added)
+            with suppress(ValueError):
+                self._on_item_added.remove(on_item_added)
 
         if on_item_removed is not None:
-            self._on_item_removed.remove(on_item_removed)
+            with suppress(ValueError):
+                self._on_item_removed.remove(on_item_removed)
 
         if on_icon_changed is not None:
-            self._on_icon_changed.remove(on_icon_changed)
+            with suppress(ValueError):
+                self._on_icon_changed.remove(on_icon_changed)
 
 
 host = StatusNotifierHost()  # noqa: E303
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/libqtile/widget/net.py 
new/qtile-0.28.1/libqtile/widget/net.py
--- old/qtile-0.27.0/libqtile/widget/net.py     2024-07-13 00:08:56.000000000 
+0200
+++ new/qtile-0.28.1/libqtile/widget/net.py     2024-08-13 01:39:10.000000000 
+0200
@@ -32,6 +32,21 @@
     """
     Displays interface down and up speed
 
+    The following fields are available in the `format` string:
+
+    - ``interface``: name of the interface
+    - ``down``: download speed
+    - ``down_suffix``: suffix for the download speed
+    - ``down_cumulative``: cumulative download traffic
+    - ``down_cumulative_suffix``: suffix for the cumulative download traffic
+    - ``up``: upload speed
+    - ``up_suffix``: suffix for the upload speed
+    - ``up_cumulative``: cumulative upload traffic
+    - ``up_cumulative_suffix``: suffix for the cumulative upload traffic
+    - ``total``: total speed
+    - ``total_suffix``: suffix for the total speed
+    - ``total_cumulative``: cumulative total traffic
+    - ``total_cumulative_suffix``: suffix for the cumulative total traffic
 
     Widget requirements: psutil_.
 
@@ -53,6 +68,11 @@
         ("update_interval", 1, "The update interval."),
         ("use_bits", False, "Use bits instead of bytes per second?"),
         ("prefix", None, "Use a specific prefix for the unit of the speed."),
+        (
+            "cumulative_prefix",
+            None,
+            "Use a specific prefix for the unit of the cumulative traffic.",
+        ),
     ]
 
     def __init__(self, **config):
@@ -82,19 +102,19 @@
                 )
         self.stats = self.get_stats()
 
-    def convert_b(self, num_bytes: float) -> tuple[float, str]:
+    def convert_b(self, num_bytes: float, prefix: str | None = None) -> 
tuple[float, str]:
         """Converts the number of bytes to the correct unit"""
 
         num_bytes *= self.byte_multiplier
 
-        if self.prefix is None:
+        if prefix is None:
             if num_bytes > 0:
                 power = int(log(num_bytes) / log(self.factor))
                 power = min(power, len(self.units) - 1)
             else:
                 power = 0
         else:
-            power = self.allowed_prefixes.index(self.prefix)
+            power = self.allowed_prefixes.index(prefix)
 
         converted_bytes = num_bytes / self.factor**power
         unit = self.units[power]
@@ -135,19 +155,34 @@
                 down = down / self.update_interval
                 up = up / self.update_interval
                 total = total / self.update_interval
-                down, down_suffix = self.convert_b(down)
-                up, up_suffix = self.convert_b(up)
-                total, total_suffix = self.convert_b(total)
+                down, down_suffix = self.convert_b(down, self.prefix)
+                down_cumulative, down_cumulative_suffix = self.convert_b(
+                    new_stats[intf]["down"], self.cumulative_prefix
+                )
+                up, up_suffix = self.convert_b(up, self.prefix)
+                up_cumulative, up_cumulative_suffix = self.convert_b(
+                    new_stats[intf]["up"], self.cumulative_prefix
+                )
+                total, total_suffix = self.convert_b(total, self.prefix)
+                total_cumulative, total_cumulative_suffix = self.convert_b(
+                    new_stats[intf]["total"], self.cumulative_prefix
+                )
                 self.stats[intf] = new_stats[intf]
                 ret_stat.append(
                     self.format.format(
                         interface=intf,
                         down=down,
                         down_suffix=down_suffix,
+                        down_cumulative=down_cumulative,
+                        down_cumulative_suffix=down_cumulative_suffix,
                         up=up,
                         up_suffix=up_suffix,
+                        up_cumulative=up_cumulative,
+                        up_cumulative_suffix=up_cumulative_suffix,
                         total=total,
                         total_suffix=total_suffix,
+                        total_cumulative=total_cumulative,
+                        total_cumulative_suffix=total_cumulative_suffix,
                     )
                 )
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/libqtile/widget/tasklist.py 
new/qtile-0.28.1/libqtile/widget/tasklist.py
--- old/qtile-0.27.0/libqtile/widget/tasklist.py        2024-07-13 
00:08:56.000000000 +0200
+++ new/qtile-0.28.1/libqtile/widget/tasklist.py        2024-08-13 
01:39:10.000000000 +0200
@@ -279,11 +279,13 @@
     @property
     def windows(self):
         if self.qtile.core.name == "x11":
-            return [
-                w
-                for w in self.bar.screen.group.windows
-                if w.window.get_wm_type() in ("normal", None)
-            ]
+            windows = []
+            for w in self.bar.screen.group.windows:
+                wm_states = list(w.window.get_property("_NET_WM_STATE", 
"ATOM", unpack=int))
+                skip_taskbar = 
w.qtile.core.conn.atoms["_NET_WM_STATE_SKIP_TASKBAR"]
+                if w.window.get_wm_type() in ("normal", None) and skip_taskbar 
not in wm_states:
+                    windows.append(w)
+            return windows
         return self.bar.screen.group.windows
 
     @property
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/libqtile/widget/wlan.py 
new/qtile-0.28.1/libqtile/widget/wlan.py
--- old/qtile-0.27.0/libqtile/widget/wlan.py    2024-07-13 00:08:56.000000000 
+0200
+++ new/qtile-0.28.1/libqtile/widget/wlan.py    2024-08-13 01:39:10.000000000 
+0200
@@ -29,6 +29,7 @@
 import iwlib
 
 from libqtile.log_utils import logger
+from libqtile.pangocffi import markup_escape_text
 from libqtile.widget import base
 
 
@@ -99,7 +100,9 @@
                         return self.disconnected_message
                 else:
                     return self.disconnected_message
-            return self.format.format(essid=essid, quality=quality, 
percent=(quality / 70))
+            return self.format.format(
+                essid=markup_escape_text(essid), quality=quality, 
percent=(quality / 70)
+            )
         except EnvironmentError:
             logger.error(
                 "Probably your wlan device is switched off or "
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/qtile.egg-info/PKG-INFO 
new/qtile-0.28.1/qtile.egg-info/PKG-INFO
--- old/qtile-0.27.0/qtile.egg-info/PKG-INFO    2024-07-13 00:09:04.000000000 
+0200
+++ new/qtile-0.28.1/qtile.egg-info/PKG-INFO    2024-08-13 01:39:18.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: qtile
-Version: 0.27.0
+Version: 0.28.1
 Summary: A pure-Python tiling window manager.
 License: MIT
 Project-URL: Homepage, https://qtile.org
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/test/widgets/test_cmus.py 
new/qtile-0.28.1/test/widgets/test_cmus.py
--- old/qtile-0.27.0/test/widgets/test_cmus.py  2024-07-13 00:08:56.000000000 
+0200
+++ new/qtile-0.28.1/test/widgets/test_cmus.py  2024-08-13 01:39:10.000000000 
+0200
@@ -26,7 +26,6 @@
 
 import libqtile.config
 from libqtile.widget import cmus
-from test.widgets.conftest import FakeBar
 
 
 class MockCmusRemoteProcess:
@@ -66,8 +65,8 @@
             [
                 "status playing",
                 "file http://playing/file/sweetcaroline.mp3";,
-                "duration 222",
-                "position 14",
+                "duration -1",
+                "position -9",
                 "tag artist Neil Diamond",
                 "tag album Greatest Hits",
                 "tag title Sweet Caroline",
@@ -131,12 +130,10 @@
         cls.call_process(cmd)
 
 
-def no_op(*args, **kwargs):
-    pass
-
-
 @pytest.fixture
-def patched_cmus(monkeypatch):
+def cmus_manager(manager_nospawn, monkeypatch, minimal_conf_noscreen, request):
+    widget_config = getattr(request, "param", dict())
+
     MockCmusRemoteProcess.reset()
     monkeypatch.setattr("libqtile.widget.cmus.subprocess", 
MockCmusRemoteProcess)
     monkeypatch.setattr(
@@ -146,104 +143,125 @@
         "libqtile.widget.cmus.base.ThreadPoolText.call_process",
         MockCmusRemoteProcess.call_process,
     )
-    return cmus
+
+    config = minimal_conf_noscreen
+    config.screens = [
+        libqtile.config.Screen(
+            top=libqtile.bar.Bar(
+                [cmus.Cmus(**widget_config)],
+                10,
+            ),
+        )
+    ]
+
+    manager_nospawn.start(config)
+    yield manager_nospawn
 
 
-def test_cmus(fake_qtile, patched_cmus, fake_window):
-    widget = patched_cmus.Cmus()
-    fakebar = FakeBar([widget], window=fake_window)
-    widget._configure(fake_qtile, fakebar)
-    text = widget.poll()
-    assert text == "♫ Rick Astley - Never Gonna Give You Up"
-    assert widget.layout.colour == widget.play_color
+def test_cmus(cmus_manager):
+    widget = cmus_manager.c.widget["cmus"]
 
-    widget.play()
-    text = widget.poll()
-    assert text == "♫ Rick Astley - Never Gonna Give You Up"
-    assert widget.layout.colour == widget.noplay_color
+    widget.eval("self.update(self.poll())")
+    assert widget.info()["text"] == "♫ Rick Astley - Never Gonna Give You Up"
+    assert widget.eval("self.layout.colour") == 
widget.eval("self.playing_color")
 
+    widget.eval("self.play()")
+    widget.eval("self.update(self.poll())")
+    assert widget.info()["text"] == "♫ Rick Astley - Never Gonna Give You Up"
+    assert widget.eval("self.layout.colour") == 
widget.eval("self.paused_color")
 
-def test_cmus_play_stopped(fake_qtile, patched_cmus, fake_window):
-    widget = patched_cmus.Cmus()
+
+def test_cmus_play_stopped(cmus_manager):
+    widget = cmus_manager.c.widget["cmus"]
 
     # Set track to a stopped item
-    MockCmusRemoteProcess.index = 2
-    fakebar = FakeBar([widget], window=fake_window)
-    widget._configure(fake_qtile, fakebar)
-    text = widget.poll()
+    widget.eval("subprocess.index = 2")
+    widget.eval("self.update(self.poll())")
 
     # It's stopped so colour should reflect this
-    assert text == "♫ tomjones"
-    assert widget.layout.colour == widget.noplay_color
+    assert widget.info()["text"] == "♫ tomjones"
+    assert widget.eval("self.layout.colour") == 
widget.eval("self.stopped_color")
 
-    widget.play()
-    text = widget.poll()
-    assert text == "♫ tomjones"
-    assert widget.layout.colour == widget.play_color
+    widget.eval("self.play()")
+    widget.eval("self.update(self.poll())")
+    assert widget.info()["text"] == "♫ tomjones"
+    assert widget.eval("self.layout.colour") == 
widget.eval("self.playing_color")
 
 
-def test_cmus_buttons(minimal_conf_noscreen, manager_nospawn, patched_cmus):
-    widget = patched_cmus.Cmus(update_interval=30)
-    config = minimal_conf_noscreen
-    config.screens = [libqtile.config.Screen(top=libqtile.bar.Bar([widget], 
10))]
-    manager_nospawn.start(config)
-    topbar = manager_nospawn.c.bar["top"]
+@pytest.mark.parametrize(
+    "cmus_manager",
+    [{"format": "{position} {duration} {position_percent} {remaining} 
{remaining_percent}"}],
+    indirect=True,
+)
+def test_cmus_times(cmus_manager):
+    widget = cmus_manager.c.widget["cmus"]
+
+    # Check item with valid position and duration
+    widget.eval("self.update(self.poll())")
+
+    # Check that times are correct
+    assert widget.info()["text"] == "00:14 03:42 6% 03:28 94%"
+
+    # Set track to an item with invalid position and duration
+    widget.eval("subprocess.index = 1")
+    widget.eval("self.update(self.poll())")
 
-    cmuswidget = manager_nospawn.c.widget["cmus"]
-    assert cmuswidget.info()["text"] == "♫ Rick Astley - Never Gonna Give 
You Up"
+    # Check that times are empty
+    assert widget.info()["text"].strip() == ""
+
+
+def test_cmus_buttons(cmus_manager):
+    topbar = cmus_manager.c.bar["top"]
+
+    widget = cmus_manager.c.widget["cmus"]
+    assert widget.info()["text"] == "♫ Rick Astley - Never Gonna Give You Up"
 
     # Play next track
-    # Non-local file source so widget just displays title
+    # Non-local file source
     topbar.fake_button_press(0, "top", 0, 0, button=4)
-    cmuswidget.eval("self.update(self.poll())")
-    assert cmuswidget.info()["text"] == "♫ Sweet Caroline"
+    widget.eval("self.update(self.poll())")
+    assert widget.info()["text"] == "♫ Neil Diamond - Sweet Caroline"
 
     # Play next track
     # Stream source so widget just displays stream info
     topbar.fake_button_press(0, "top", 0, 0, button=4)
-    cmuswidget.eval("self.update(self.poll())")
-    assert cmuswidget.info()["text"] == "♫ tomjones"
+    widget.eval("self.update(self.poll())")
+    assert widget.info()["text"] == "♫ tomjones"
 
     # Play previous track
-    # Non-local file source so widget just displays title
+    # Non-local file source
     topbar.fake_button_press(0, "top", 0, 0, button=5)
-    cmuswidget.eval("self.update(self.poll())")
-    assert cmuswidget.info()["text"] == "♫ Sweet Caroline"
+    widget.eval("self.update(self.poll())")
+    assert widget.info()["text"] == "♫ Neil Diamond - Sweet Caroline"
 
 
-def test_cmus_error_handling(fake_qtile, patched_cmus, fake_window):
-    widget = patched_cmus.Cmus()
-    MockCmusRemoteProcess.is_error = True
-    fakebar = FakeBar([widget], window=fake_window)
-    widget._configure(fake_qtile, fakebar)
-    text = widget.poll()
+def test_cmus_error_handling(cmus_manager):
+    widget = cmus_manager.c.widget["cmus"]
+    widget.eval("subprocess.is_error = True")
+    widget.eval("self.update(self.poll())")
 
     # Widget does nothing with error message so text is blank
     # TODO: update widget to show error?
-    assert text == ""
+    assert widget.info()["text"] == ""
 
 
-def test_escape_text(fake_qtile, patched_cmus, fake_window):
-    widget = patched_cmus.Cmus()
+def test_escape_text(cmus_manager):
+    widget = cmus_manager.c.widget["cmus"]
 
-    # Set track to a stopped item
-    MockCmusRemoteProcess.index = 3
-    fakebar = FakeBar([widget], window=fake_window)
-    widget._configure(fake_qtile, fakebar)
-    text = widget.poll()
+    # Set track to an item with a title which needs escaping
+    widget.eval("subprocess.index = 3")
+    widget.eval("self.update(self.poll())")
 
-    # It's stopped so colour should reflect this
-    assert text == "♫ Above &amp; Beyond - Always - Tinlicker Extended Mix"
+    # & should be escaped to &amp;
+    assert widget.info()["text"] == "♫ Above &amp; Beyond - Always - 
Tinlicker Extended Mix"
 
 
-def test_missing_metadata(fake_qtile, patched_cmus, fake_window):
-    widget = patched_cmus.Cmus()
+def test_missing_metadata(cmus_manager):
+    widget = cmus_manager.c.widget["cmus"]
 
     # Set track to one that's missing Title and Artist metadata
-    MockCmusRemoteProcess.index = 4
-    fakebar = FakeBar([widget], window=fake_window)
-    widget._configure(fake_qtile, fakebar)
-    text = widget.poll()
+    widget.eval("subprocess.index = 4")
+    widget.eval("self.update(self.poll())")
 
     # Displayed text should default to the name of the file
-    assert text == "♫ always.mp3"
+    assert widget.info()["text"] == "♫ always.mp3"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/test/widgets/test_net.py 
new/qtile-0.28.1/test/widgets/test_net.py
--- old/qtile-0.27.0/test/widgets/test_net.py   2024-07-13 00:08:56.000000000 
+0200
+++ new/qtile-0.28.1/test/widgets/test_net.py   2024-08-13 01:39:10.000000000 
+0200
@@ -62,7 +62,9 @@
         # Reload fixes cases where psutil may have been imported previously
         reload(net)
         widget = net.Net(
-            format="{interface}: U {up}{up_suffix} D {down}{down_suffix} T 
{total}{total_suffix}",
+            format="{interface}: U {up}{up_suffix} 
{up_cumulative}{up_cumulative_suffix} D "
+            "{down}{down_suffix} {down_cumulative}{down_cumulative_suffix} T 
{total}"
+            "{total_suffix} {total_cumulative}{total_cumulative_suffix}",
             **kwargs
         )
         fakebar = FakeBar([widget], window=fake_window)
@@ -76,19 +78,22 @@
 def test_net_defaults(patch_net):
     """Default: widget shows `all` interfaces"""
     net1 = patch_net()
-    assert net1.poll() == "all: U 40.0kB D 1.2MB T 1.24MB"
+    assert net1.poll() == "all: U 40.0kB 80.0kB D 1.2MB 2.4MB T 1.24MB 2.48MB"
 
 
 def test_net_single_interface(patch_net):
     """Display single named interface"""
     net2 = patch_net(interface="wlp58s0")
-    assert net2.poll() == "wlp58s0: U 40.0kB D 1.2MB T 1.24MB"
+    assert net2.poll() == "wlp58s0: U 40.0kB 160.0kB D 1.2MB 4.8MB T 1.24MB 
4.96MB"
 
 
 def test_net_list_interface(patch_net):
     """Display multiple named interfaces"""
     net2 = patch_net(interface=["wlp58s0", "lo"])
-    assert net2.poll() == "wlp58s0: U 40.0kB D 1.2MB T 1.24MB lo: U 40.0kB D 
1.2MB T 1.24MB"
+    assert net2.poll() == (
+        "wlp58s0: U 40.0kB 240.0kB D 1.2MB 7.2MB T 1.24MB 7.44MB lo: U 40.0kB "
+        "240.0kB D 1.2MB 7.2MB T 1.24MB 7.44MB"
+    )
 
 
 def test_net_invalid_interface(patch_net):
@@ -100,7 +105,7 @@
 def test_net_use_bits(patch_net):
     """Display all interfaces in bits rather than bytes"""
     net4 = patch_net(use_bits=True)
-    assert net4.poll() == "all: U 320.0kb D 9.6Mb T 9.92Mb"
+    assert net4.poll() == "all: U 320.0kb 2.56Mb D 9.6Mb 76.8Mb T 9.92Mb 
79.36Mb"
 
 
 def test_net_convert_zero_b(patch_net):
@@ -112,7 +117,7 @@
 def test_net_use_prefix(patch_net):
     """Tests `prefix` configurable option"""
     net6 = patch_net(prefix="M")
-    assert net6.poll() == "all: U 0.04MB D 1.2MB T 1.24MB"
+    assert net6.poll() == "all: U 0.04MB 440.0kB D 1.2MB 13.2MB T 1.24MB 
13.64MB"
 
 
 # Untested: 128-129 - generic exception catching
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/test/widgets/test_tasklist.py 
new/qtile-0.28.1/test/widgets/test_tasklist.py
--- old/qtile-0.27.0/test/widgets/test_tasklist.py      2024-07-13 
00:08:56.000000000 +0200
+++ new/qtile-0.28.1/test/widgets/test_tasklist.py      2024-08-13 
01:39:10.000000000 +0200
@@ -17,6 +17,7 @@
 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
+
 import pytest
 
 import libqtile.config
@@ -24,6 +25,8 @@
 from libqtile.config import Screen
 from libqtile.confreader import Config
 from libqtile.widget.tasklist import TaskList
+from test.layouts.layout_utils import assert_focused
+from test.test_scratchpad import is_spawned, spawn_cmd
 
 
 class TaskListTestWidget(TaskList):
@@ -59,7 +62,16 @@
 
     class TasklistConfig(Config):
         auto_fullscreen = True
-        groups = [libqtile.config.Group("a"), libqtile.config.Group("b")]
+        groups = [
+            libqtile.config.ScratchPad(
+                "SCRATCHPAD",
+                dropdowns=[
+                    libqtile.config.DropDown("dd-a", spawn_cmd("dd-a"), 
on_focus_lost_hide=False),
+                ],
+            ),
+            libqtile.config.Group("a"),
+            libqtile.config.Group("b"),
+        ]
         layouts = [layout.Stack()]
         floating_layout = libqtile.resources.default_config.floating_layout
         keys = []
@@ -104,6 +116,37 @@
     assert widget.info()["text"] == "One|Two"
 
 
+def test_tasklist_skip_taskbar_defaults(tasklist_manager):
+    widget = tasklist_manager.c.widget["tasklist"]
+    tasklist_manager.c.group["SCRATCHPAD"].dropdown_reconfigure("dd-a")
+
+    tasklist_manager.test_window("one")
+    assert_focused(tasklist_manager, "one")
+
+    # dd-a has no window associated yet
+    assert "window" not in 
tasklist_manager.c.group["SCRATCHPAD"].dropdown_info("dd-a")
+
+    # First toggling: wait for window
+    tasklist_manager.c.group["SCRATCHPAD"].dropdown_toggle("dd-a")
+    is_spawned(tasklist_manager, "dd-a")
+    assert_focused(tasklist_manager, "dd-a")
+    assert (
+        
tasklist_manager.c.group["SCRATCHPAD"].dropdown_info("dd-a")["window"]["name"] 
== "dd-a"
+    )
+
+    if tasklist_manager.c.core.info()["backend"] == "x11":
+        # check that window's _NET_WM_STATE contains _NET_WM_STATE_SKIP_TASKBAR
+        net_wm_state = tasklist_manager.c.window.eval(
+            'self.window.get_property("_NET_WM_STATE", "ATOM", unpack=int)'
+        )[1]
+        skip_taskbar = tasklist_manager.c.window.eval(
+            'self.qtile.core.conn.atoms["_NET_WM_STATE_SKIP_TASKBAR"]'
+        )[1]
+        assert skip_taskbar in net_wm_state
+        assert tasklist_manager.c.window.eval("self.window.get_wm_type()")[1] 
== "normal"
+        assert widget.info()["text"] == "one"
+
+
 @configure_tasklist(txt_minimized="(min) ", txt_maximized="(max) ", 
txt_floating="(float) ")
 def test_tasklist_custom_text(tasklist_manager):
     widget = tasklist_manager.c.widget["tasklist"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/qtile-0.27.0/test/widgets/test_wlan.py 
new/qtile-0.28.1/test/widgets/test_wlan.py
--- old/qtile-0.27.0/test/widgets/test_wlan.py  2024-07-13 00:08:56.000000000 
+0200
+++ new/qtile-0.28.1/test/widgets/test_wlan.py  2024-08-13 01:39:10.000000000 
+0200
@@ -103,6 +103,15 @@
     assert text == expected
 
 
+def test_wlan_display_escape_essid(
+    minimal_conf_noscreen, manager_nospawn, patched_wlan, monkeypatch
+):
+    """Test escaping of pango markup in ESSID"""
+    monkeypatch.setitem(MockIwlib.DATA["wlan0"], "ESSID", b"A&B")
+    widget = patched_wlan.Wlan(format="{essid}")
+    assert widget.poll() == "A&amp;B"
+
+
 @pytest.mark.parametrize(
     "kwargs,state,expected",
     [

Reply via email to