How to get this pending updates notification in Gnome?

Thanks for the note @rofrol! I’ll offer as much as I can here, my plan is to eventually publish my home-manager configuration once it’s complete, but I’m not quite done yet and it needs some additional polish (technically, scrubbing) before I upload it.

Here’s the relevant details:

Technically I’m using the excellent devos by @nrdxp, so I symlink /etc/nixos to my local checkout of my devos repository. So /etc/nixos is a devos repository, but can still serve as a place to nix build, etc.

The interface in the screenshot is i3, polybar, and dunst. The widget in the lower left is this bit of polybar configuration:

[module/updates]
click-left=kill -USR1 %pid%
click-right=kill -USR2 %pid%
exec=/nix/store/jh57cw9p3p40z1c9c4ncvp6bli5ay3n8-packages/bin/packages --icon /nix/store/8qxzbn358fm7nq55w202ypjkizsg0dmc
-nixos-icons-2021-02-24/share/icons/hicolor/scalable/apps/nix-snowflake.svg
format-prefix=" "
format-prefix-foreground=${colors.foreground-alt}
format-suffix=" ↑"
format-suffix-foreground=${colors.foreground-alt}
format-underline=#4bffdc
tail=true
type=custom/script

The packages executable is a python script I have that checks for updates on various distros. I suppose I’ll just copypasta that here, it’s a private script that I haven’t put anywhere else so it probably makes some assumptions that may break for others, but it may be helpful regardless.

packages.py
from pathlib import Path
from subprocess import DEVNULL, PIPE, run
from threading import Event

import argparse
import crayons
import dbus
import distro
import os
import signal
import sys

parser = argparse.ArgumentParser(
    description="Mini-daemon for *bar update widgets")
parser.add_argument(
    "--icon", default='system-software-update',
    type=Path, help="Which icon to use"
)
args = parser.parse_args()

os.environ['PATH'] += os.pathsep + os.pathsep.join([
    '/run/current-system/sw/bin',
    os.path.sep.join([os.path.expanduser('~'), '.nix-profile', 'bin'])
])

bus = "org.freedesktop.Notifications"
delay = 60 * 60

AVAILABLE = []
CACHE = Path.home() / '.cache' / 'packages-pending'
RUN = Event()
BUS_INTERFACE = dbus.Interface(
    dbus.SessionBus().get_object(bus, "/"+bus.replace(".", "/")), bus)

distribution = distro.name()
if 'Arch' in distribution:
    CHECKER = ['checkupdates']
elif 'Fedora' in distribution:
    CHECKER = ['dnf', 'updateinfo', '-q', '--list']
elif 'Nix' in distribution:
    CHECKER = ['nix-updateinfo']
else:
    print("Unknown distro")
    sys.exit(1)


def discharging():
    for supply in Path('/sys/class/power_supply').iterdir():
        if '/usb' in str(supply.resolve()):
            continue
        status = supply / 'status'
        if status.exists():
            with status.open('r') as f:
                if f.read().strip() == 'Discharging':
                    return True
    return False


def updates():
    global CHECKER
    return crayons.clean(
        run(CHECKER, stdout=PIPE, stderr=DEVNULL).stdout.decode('utf-8')
    ).split('\n')


def check():
    print("↺", flush=True)
    global AVAILABLE
    global CACHE
    cached = False
    if discharging() and CACHE.exists():
        with CACHE.open('r') as f:
            AVAILABLE = list(map(lambda x: x.strip(), f.readlines()))
            cached = True
    else:
        try:
            AVAILABLE = list(filter(bool, updates()))
        except Exception:
            print("!", flush=True)
            return

    results = len(AVAILABLE)
    if results > 0:
        print(f"{results}{'💤' if cached else ''}", flush=True)
    else:
        print("✓", flush=True)

    with CACHE.open('w') as f:
        f.write('\n'.join(AVAILABLE))


def handle_force(sig, frame):
    global RUN
    RUN.set()


def handle_notify(sig, frame):
    global AVAILABLE
    global BUS_INTERFACE
    global args
    BUS_INTERFACE.Notify(
        "System Updates", 0, str(args.icon), "Available Updates",
        '\n'.join(AVAILABLE), [], {"urgency": 1}, 5000)


signal.signal(signal.SIGUSR1, handle_force)
signal.signal(signal.SIGUSR2, handle_notify)

RUN.set()
while True:
    RUN.wait(delay)
    check()
    RUN.clear()

nix-updateinfo is actually pretty short, this is the script. I’ll include what I have in my home-manager config verbatim:

nix-updates-script = pkgs.writers.writeBashBin "nix-updateinfo" ''
  tempdir=$(mktemp -d /tmp/tmp.nix-updateinfo.XXX)
  ${pkgs.git}/bin/git clone --reference /etc/nixos /etc/nixos $tempdir
  cd $tempdir
  # Disable the noisy devshell git hook provisioner
  sed -i 's/true/false/' shell/hooks/default.nix || true
  ${pkgs.nixUnstable}/bin/nix-shell --run "bud update latest"
  ${pkgs.nixUnstable}/bin/nix-shell --run "bud build toplevel"
  ${pkgs.nixUnstable}/bin/nix store diff-closures /run/current-system ./result | awk '/[0-9] →|→ [0-9]/ && !/nixos/' || echo
  cd ~-
  rm -rf "$tempdir"
'';

For those not using devos, you can achieve the “update nixpkgs” step by either calling nix flake update <your nixpkgs input> or whatever the equivalent is for !flakes. Running nix-updateinfo from a normal shell should yield pending updates outside of the polybar widget.

The visual notification is from dunst; my configuration for that is pretty large but I’ll drop it here so that anyone can truly reproduce the setup if they want:

~/.config/dunst/dunstrc
[experimental]
per_monitor_dpi=yes

[global]
alignment="left"
always_run_script=yes
browser="/nix/store/kqdhiygq1rzbq1cpnhix29dq7p6a97z5-firefox-94.0.1/bin/firefox -new-tab"
class="Dunst"
corner_radius=5
dmenu="/nix/store/yf9sdd85amgn63kjkzhgi9gpm6lm2sbi-rofi-1.7.0/bin/rofi -dmenu -p Notification"
ellipsize="middle"
follow="mouse"
font="fixed 12"
format="<b>%s</b>\n\n%b\n\n<sub>%a</sub>"
frame_color="#282a36"
frame_width=3
hide_duplicate_count=no
history_length=20
horizontal_padding=20
icon_path="/run/current-system/sw/share/icons/Adwaita/32x32/actions:/run/current-system/sw/share/icons/hicolor/32x32/actions:/run/current-system/sw/share/icons/Adwaita/32x32/animations:/run/current-system/sw/share/icons/hicolor/32x32/animations:/run/current-system/sw/share/icons/Adwaita/32x32/apps:/run/current-system/sw/share/icons/hicolor/32x32/apps:/run/current-system/sw/share/icons/Adwaita/32x32/categories:/run/current-system/sw/share/icons/hicolor/32x32/categories:/run/current-system/sw/share/icons/Adwaita/32x32/devices:/run/current-system/sw/share/icons/hicolor/32x32/devices:/run/current-system/sw/share/icons/Adwaita/32x32/emblems:/run/current-system/sw/share/icons/hicolor/32x32/emblems:/run/current-system/sw/share/icons/Adwaita/32x32/emotes:/run/current-system/sw/share/icons/hicolor/32x32/emotes:/run/current-system/sw/share/icons/Adwaita/32x32/filesystem:/run/current-system/sw/share/icons/hicolor/32x32/filesystem:/run/current-system/sw/share/icons/Adwaita/32x32/intl:/run/current-system/sw/share/icons/hicolor/32x32/intl:/run/current-system/sw/share/icons/Adwaita/32x32/legacy:/run/current-system/sw/share/icons/hicolor/32x32/legacy:/run/current-system/sw/share/icons/Adwaita/32x32/mimetypes:/run/current-system/sw/share/icons/hicolor/32x32/mimetypes:/run/current-system/sw/share/icons/Adwaita/32x32/places:/run/current-system/sw/share/icons/hicolor/32x32/places:/run/current-system/sw/share/icons/Adwaita/32x32/status:/run/current-system/sw/share/icons/hicolor/32x32/status:/run/current-system/sw/share/icons/Adwaita/32x32/stock:/run/current-system/sw/share/icons/hicolor/32x32/stock:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/actions:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/actions:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/animations:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/animations:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/apps:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/apps:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/categories:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/categories:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/devices:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/devices:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/emblems:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/emblems:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/emotes:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/emotes:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/filesystem:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/filesystem:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/intl:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/intl:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/legacy:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/legacy:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/mimetypes:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/mimetypes:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/places:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/places:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/status:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/status:/etc/profiles/per-user/tylerjl/share/icons/Adwaita/32x32/stock:/etc/profiles/per-user/tylerjl/share/icons/hicolor/32x32/stock:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/actions:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/actions:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/animations:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/animations:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/apps:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/apps:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/categories:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/categories:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/devices:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/devices:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/emblems:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/emblems:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/emotes:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/emotes:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/filesystem:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/filesystem:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/intl:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/intl:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/legacy:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/legacy:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/mimetypes:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/mimetypes:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/places:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/places:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/status:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/status:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/Adwaita/32x32/stock:/nix/store/8hp8qiqps1gm3ibscmjgz5wa4a2q851v-adwaita-icon-theme-41.0/share/icons/hicolor/32x32/stock:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/actions:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/actions:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/animations:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/animations:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/apps:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/apps:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/categories:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/categories:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/devices:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/devices:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/emblems:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/emblems:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/emotes:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/emotes:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/filesystem:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/filesystem:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/intl:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/intl:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/legacy:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/legacy:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/mimetypes:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/mimetypes:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/places:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/places:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/status:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/status:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/Adwaita/32x32/stock:/nix/store/gz1r222x6s65lj091fl67c35g11hismj-hicolor-icon-theme-0.17/share/icons/hicolor/32x32/stock"
icon_position="left"
idle_threshold=120
ignore_newline="no"
indicate_hidden="yes"
line_height=0
markup="full"
max_icon_size=64
monitor=0
mouse_left_click="close_current"
mouse_middle_click="close_all"
mouse_right_click="do_action"
notification_limit="5"
offset="20x75"
origin="bottom-right"
padding=8
separator_color="frame"
separator_height=2
show_age_threshold=60
show_indicators="yes"
shrink="no"
sort="yes"
stack_duplicates=yes
sticky_history="yes"
title="Dunst"
transparency=30
width="(0, 800)"
word_wrap="yes"

[urgency_critical]
background="#ab4642"
foreground="#e8e8e8"
frame_color="#ff0000"
timeout=0

[urgency_low]
background="#282828"
foreground="#585858"
timeout=10

[urgency_normal]
background="#282a3690"
foreground="#f5f5f5"
timeout=10

Note that all of these configs (polybar, dunst, etc.) are all configured via the relevant home-manager settings so all of this lives in a .nix file in my machine’s configuration. That should be enough (I think?) to explain the setup.

I hope that’s helpful, let me know if there’s anything else I can clarify!

2 Likes