diff --git a/i3pystatus/updates/__init__.py b/i3pystatus/updates/__init__.py index eee9e90..3e2beb2 100644 --- a/i3pystatus/updates/__init__.py +++ b/i3pystatus/updates/__init__.py @@ -2,6 +2,7 @@ import threading from i3pystatus import SettingsBase, Module, formatp from i3pystatus.core.util import internet, require +from i3pystatus.core.desktop import DesktopNotification class Backend(SettingsBase): @@ -18,13 +19,17 @@ class Updates(Module): Left clicking on the module will refresh the count of upgradeable packages. This may be used to dismiss the notification after updating your system. + Right clicking shows a desktop notification with a summary count and a list + of available updates. + .. rubric:: Available formatters * `{count}` — Sum of all available updates from all backends. - * For each backend registered there is one formatter named after the backend, - multiple identical backends do not accumulate, but overwrite each other. - * For example, `{Cower}` (note capitcal C) is the number of updates reported by - the cower backend, assuming it has been registered. + * For each backend registered there is one formatter named after the + backend, multiple identical backends do not accumulate, but overwrite + each other. + * For example, `{Cower}` (note capital C) is the number of updates + reported by the cower backend, assuming it has been registered. .. rubric:: Usage example @@ -50,9 +55,16 @@ class Updates(Module): ("backends", "Required list of backends used to check for updates."), ("format", "Format used when updates are available. " "May contain formatters."), - ("format_no_updates", "String that is shown if no updates are available." - " If not set the module will be hidden if no updates are available."), - ("format_working", "Format used while update queries are run. By default the same as ``format``."), + ("format_no_updates", "String that is shown if no updates are " + "available. If not set the module will be hidden if no updates " + "are available."), + ("format_working", "Format used while update queries are run. By " + "default the same as ``format``."), + ("format_summary", "Format for the summary line of notifications. By " + "default the same as ``format``."), + ("notification_icon", "Icon shown when reporting the list of updates. " + "Default is ``software-update-available``, and can be " + "None for no icon."), "color", "color_no_updates", "color_working", @@ -64,21 +76,27 @@ class Updates(Module): format = "Updates: {count}" format_no_updates = None format_working = None + format_summary = None + notification_icon = "software-update-available" color = "#00DD00" color_no_updates = None color_working = None on_leftclick = "run" + on_rightclick = "report" def init(self): if not isinstance(self.backends, list): self.backends = [self.backends] if self.format_working is None: # we want to allow an empty format self.format_working = self.format + if self.format_summary is None: # we want to allow an empty format + self.format_summary = self.format self.color_working = self.color_working or self.color self.data = { "count": 0 } + self.notif_body = {} self.condition = threading.Condition() self.thread = threading.Thread(target=self.update_thread, daemon=True) self.thread.start() @@ -95,7 +113,9 @@ class Updates(Module): for backend in self.backends: key = backend.__class__.__name__ if key not in self.data: - self.data[key] = '?' + self.data[key] = "?" + if key not in self.notif_body: + self.notif_body[key] = "" self.output = { "full_text": formatp(self.format_working, **self.data).strip(), @@ -104,9 +124,11 @@ class Updates(Module): updates_count = 0 for backend in self.backends: - updates = backend.updates + name = backend.__class__.__name__ + updates, notif_body = backend.updates updates_count += updates - self.data[backend.__class__.__name__] = updates + self.data[name] = updates + self.notif_body[name] = notif_body or "" if updates_count == 0: self.output = {} if not self.format_no_updates else { @@ -124,3 +146,12 @@ class Updates(Module): def run(self): with self.condition: self.condition.notify() + + def report(self): + DesktopNotification( + title=formatp(self.format_summary, **self.data).strip(), + body="\n".join(self.notif_body.values()), + icon=self.notification_icon, + urgency=1, + timeout=0, + ).display() diff --git a/i3pystatus/updates/aptget.py b/i3pystatus/updates/aptget.py index f73c733..432f520 100644 --- a/i3pystatus/updates/aptget.py +++ b/i3pystatus/updates/aptget.py @@ -23,10 +23,14 @@ class AptGet(Backend): command = "apt-get upgrade -s -o Dir::State::Lists=" + cache_dir apt = run_through_shell(command.split()) - update_count = 0 - for line in apt.out.split("\n"): - if line.startswith("Inst"): - update_count += 1 - return update_count + out = apt.out.splitlines() + out = [line[5:] for line in apt.out if line.startswith("Inst ")] + return out.count("\n"), out Backend = AptGet + +if __name__ == "__main__": + """ + Call this module directly; Print the update count and notification body. + """ + print("Updates: {}\n\n{}".format(*Backend().updates)) diff --git a/i3pystatus/updates/cower.py b/i3pystatus/updates/cower.py index 144f156..2700444 100644 --- a/i3pystatus/updates/cower.py +++ b/i3pystatus/updates/cower.py @@ -13,6 +13,12 @@ class Cower(Backend): def updates(self): command = ["cower", "-u"] cower = run_through_shell(command) - return cower.out.count('\n') + return cower.out.count('\n'), cower.out Backend = Cower + +if __name__ == "__main__": + """ + Call this module directly; Print the update count and notification body. + """ + print("Updates: {}\n\n{}".format(*Backend().updates)) diff --git a/i3pystatus/updates/dnf.py b/i3pystatus/updates/dnf.py index efd4bda..c57ee4b 100644 --- a/i3pystatus/updates/dnf.py +++ b/i3pystatus/updates/dnf.py @@ -1,11 +1,14 @@ from i3pystatus.core.command import run_through_shell from i3pystatus.updates import Backend -from re import split +from re import split, sub class Dnf(Backend): """ - Gets update count for RPM-based distributions with dnf. + Gets updates for RPM-based distributions with `dnf check-update`. + + The notification body consists of the status line followed by the package + name and version for each update. https://dnf.readthedocs.org/en/latest/command_ref.html#check-update-command """ @@ -14,12 +17,22 @@ class Dnf(Backend): def updates(self): command = ["dnf", "check-update"] dnf = run_through_shell(command) + if dnf.err: + return "?", dnf.err + raw = dnf.out update_count = 0 if dnf.rc == 100: - lines = dnf.out.splitlines()[2:] - lines = [l for l in lines if len(split("\s{2,}", l.rstrip())) == 3] + lines = raw.splitlines()[2:] + lines = [l for l in lines if len(split("\s+", l.rstrip())) == 3] update_count = len(lines) - return update_count + notif_body = sub(r"(\S+)\s+(\S+)\s+\S+\s*\n", r"\1: \2\n", raw) + return update_count, notif_body Backend = Dnf + +if __name__ == "__main__": + """ + Call this module directly; Print the update count and notification body. + """ + print("Updates: {}\n\n{}".format(*Backend().updates)) diff --git a/i3pystatus/updates/pacman.py b/i3pystatus/updates/pacman.py index 8f40945..45ac537 100644 --- a/i3pystatus/updates/pacman.py +++ b/i3pystatus/updates/pacman.py @@ -12,6 +12,12 @@ class Pacman(Backend): def updates(self): command = ["checkupdates"] checkupdates = run_through_shell(command) - return checkupdates.out.count('\n') + return checkupdates.out.count("\n"), checkupdates.out Backend = Pacman + +if __name__ == "__main__": + """ + Call this module directly; Print the update count and notification body. + """ + print("Updates: {}\n\n{}".format(*Backend().updates)) diff --git a/i3pystatus/updates/yaourt.py b/i3pystatus/updates/yaourt.py index 3738ff6..9489a42 100644 --- a/i3pystatus/updates/yaourt.py +++ b/i3pystatus/updates/yaourt.py @@ -1,4 +1,3 @@ -import re from i3pystatus.core.command import run_through_shell from i3pystatus.updates import Backend @@ -6,16 +5,22 @@ from i3pystatus.updates import Backend class Yaourt(Backend): """ This module counts the available updates using yaourt. - By default it will only count aur packages. Thus it can be used with the pacman backend like this: + By default it will only count aur packages. Thus it can be used with the + pacman backend like this: - from i3pystatus.updates import pacman, yaourt - status.register("updates", backends = [pacman.Pacman(), yaourt.Yaourt()]) + .. code-block:: python - If you want to count both pacman and aur packages with this module you can set the variable - count_only_aur = False like this: + from i3pystatus.updates import pacman, yaourt + status.register("updates", backends = \ +[pacman.Pacman(), yaourt.Yaourt()]) - from i3pystatus.updates import yaourt - status.register("updates", backends = [yaourt.Yaourt(False)]) + If you want to count both pacman and aur packages with this module you can + set the variable count_only_aur = False like this: + + .. code-block:: python + + from i3pystatus.updates import yaourt + status.register("updates", backends = [yaourt.Yaourt(False)]) """ def __init__(self, aur_only=True): @@ -25,8 +30,15 @@ class Yaourt(Backend): def updates(self): command = ["yaourt", "-Qua"] checkupdates = run_through_shell(command) + out = checkupdates.out if(self.aur_only): - return len(re.findall("^aur/", checkupdates.out, flags=re.M)) - return checkupdates.out.count("\n") + out = [line for line in out if line.startswith("aur")] + return out.count("\n"), out Backend = Yaourt + +if __name__ == "__main__": + """ + Call this module directly; Print the update count and notification body. + """ + print("Updates: {}\n\n{}".format(*Backend().updates))