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 & Beyond - Always - Tinlicker Extended Mix" + # & should be escaped to & + assert widget.info()["text"] == "â« Above & 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&B" + + @pytest.mark.parametrize( "kwargs,state,expected", [