Files
Nomarchy/modules/home/updates.nix
Bernardo Magri 6a4af69b0f feat(updates): passive update-awareness indicator + notification
Add opt-in nomarchy.updates (modules/home/updates.nix): a nomarchy-updates
checker on a systemd user timer (interval, default daily) that surfaces a
Waybar indicator + a notification when updates are available, without ever
changing anything (you still run sys-update / flatpak update).

It counts:
- flake inputs behind upstream — only the flake's DIRECT inputs (root.inputs:
  nixpkgs, the Nomarchy input, home-manager, stylix…), not the transitive
  closure, via `git ls-remote` vs the locked rev. Offline → skipped, never a
  false alarm.
- Flatpak updates, when the flatpak CLI is present (.flatpak, gated on
  services.flatpak being on) — Bernardo's suggestion.

Waybar custom/updates self-gates (hidden until the timer finds something;
accent "󰚰 N", signal 9 for instant refresh), in the generated bar and both
summer whole-swaps. The notification fires only when the count grows, so a
daily timer doesn't nag. Click → the upgrade flow in a terminal (sys-update /
flatpak update, each confirmed). nomarchy-updates ships always on PATH and
self-gates so the static whole-swap bars can exec it even when the feature
is off.

Verified: flake check clean; home generation builds; the jq direct-input
filter + status self-gate exercised against the real flake.lock; script
passes bash -n. Pending an on-machine check (ls-remote/notify/timer need a
live session).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:59:09 +01:00

142 lines
6.0 KiB
Nix

# Update awareness (opt-in, nomarchy.updates.enable) — a passive background
# check that surfaces a Waybar indicator + a notification when updates are
# available, without ever changing anything (you still run sys-update /
# home-update / flatpak update). It counts:
# • flake inputs whose locked rev is behind upstream (nixpkgs, the Nomarchy
# input, home-manager …) — via `git ls-remote` on each branch-tracking
# github/git input in flake.lock; offline → skipped, never a false alarm.
# • Flatpak updates, when the `flatpak` CLI is present (services.flatpak on).
#
# nomarchy-updates is always on PATH and self-gates (status prints nothing
# until the timer has found something), so the Waybar module — generated and
# whole-swap — can exec it by name even when the feature is off.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy.updates;
nomarchy-updates = pkgs.writeShellScriptBin "nomarchy-updates" ''
set -u
# System + user profiles, so flatpak / sys-update resolve from a timer-run
# service too (build-time tools below use absolute store paths regardless).
export PATH="$PATH:/run/current-system/sw/bin:/etc/profiles/per-user/$USER/bin"
GIT=${pkgs.git}/bin/git
JQ=${pkgs.jq}/bin/jq
cache="''${XDG_CACHE_HOME:-$HOME/.cache}/nomarchy"
state="$cache/updates.json"
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
mkdir -p "$cache"
count_nix() {
local lock="$flake/flake.lock" n=0 name kind owner repo url ref locked giturl up
[ -f "$lock" ] || { echo 0; return; }
while IFS=$'\t' read -r name kind owner repo url ref locked; do
[ -n "$locked" ] || continue
case "$kind" in
github) giturl="https://github.com/$owner/$repo" ;;
git) giturl="$url" ;;
*) continue ;;
esac
up=$("$GIT" ls-remote "$giturl" "$ref" 2>/dev/null | ${pkgs.gawk}/bin/awk 'NR==1{print $1}')
[ -n "$up" ] || continue # offline / unknown skip (no false alarm)
[ "$up" != "$locked" ] && n=$((n + 1))
done < <(
# Only the flake's DIRECT inputs (root.inputs) not the transitive
# closure so a deep dependency bump doesn't nag as an "update".
"$JQ" -r '
.nodes as $nodes
| ($nodes.root.inputs | [ .[] | if type == "array" then .[0] else . end ]) as $direct
| $nodes | to_entries[]
| .key as $k | .value as $v
| select($direct | index($k))
| select(($v.original.type? == "github") or ($v.original.type? == "git"))
| select(($v.original.rev? // "") == "") # branch-tracking only
| [ $k, $v.original.type,
($v.original.owner? // ""), ($v.original.repo? // ""),
($v.original.url? // ""), ($v.original.ref? // "HEAD"),
($v.locked.rev? // "") ] | @tsv
' "$lock"
)
echo "$n"
}
count_flatpak() {
${lib.optionalString cfg.flatpak ''
if command -v flatpak >/dev/null 2>&1; then
flatpak remote-ls --updates --columns=application 2>/dev/null | ${pkgs.gnugrep}/bin/grep -c . || true
return
fi
''}
echo 0
}
refresh_bar() { ${pkgs.procps}/bin/pkill -RTMIN+9 -x waybar 2>/dev/null || true; }
case "''${1:-status}" in
check)
nix=$(count_nix); fp=$(count_flatpak); total=$((nix + fp))
prev=$("$JQ" -r '.total // 0' "$state" 2>/dev/null || echo 0)
printf '{"nix":%d,"flatpak":%d,"total":%d,"ts":%d}\n' \
"$nix" "$fp" "$total" "$(${pkgs.coreutils}/bin/date +%s)" > "$state"
# Notify only when NEW updates appear, so a daily timer doesn't nag.
if [ "$total" -gt 0 ] && [ "$total" -gt "$prev" ]; then
msg="$nix flake input(s)"
[ "$fp" -gt 0 ] && msg="$msg · $fp Flatpak(s)"
${pkgs.libnotify}/bin/notify-send -a Nomarchy "Updates available" \
"$msg click the bar icon, or run sys-update."
fi
refresh_bar ;;
status)
total=$("$JQ" -r '.total // 0' "$state" 2>/dev/null || echo 0)
[ "$total" -gt 0 ] 2>/dev/null || exit 0 # up to date / unchecked hide
nix=$("$JQ" -r '.nix // 0' "$state"); fp=$("$JQ" -r '.flatpak // 0' "$state")
tip="Updates available"
[ "$nix" -gt 0 ] && tip="$tip\n $nix flake input(s) sys-update"
[ "$fp" -gt 0 ] && tip="$tip\n $fp Flatpak(s) flatpak update"
printf '{"text":"󰚰 %d","tooltip":"%s","class":"available"}\n' "$total" "$tip" ;;
upgrade)
echo "Checking"; "$0" check
nix=$("$JQ" -r '.nix // 0' "$state" 2>/dev/null || echo 0)
fp=$("$JQ" -r '.flatpak // 0' "$state" 2>/dev/null || echo 0)
echo "Pending: $nix flake input(s), $fp Flatpak(s)."
if [ "$nix" -gt 0 ] && command -v sys-update >/dev/null 2>&1; then
read -rp "Run sys-update (flake update + system rebuild)? [y/N] " a
[ "$a" = y ] && sys-update
fi
if [ "$fp" -gt 0 ] && command -v flatpak >/dev/null 2>&1; then
read -rp "Run flatpak update? [y/N] " a
[ "$a" = y ] && flatpak update
fi
"$0" check
echo "Done press enter."; read -r _ || true ;;
*) echo "usage: nomarchy-updates [check|status|upgrade]" >&2; exit 64 ;;
esac
'';
in
{
config = lib.mkMerge [
# Always on PATH so the Waybar module (incl. the static whole-swap themes)
# can exec it; it self-gates at runtime.
{ home.packages = [ nomarchy-updates ]; }
(lib.mkIf cfg.enable {
systemd.user.services.nomarchy-updates = {
Unit.Description = "Check for Nomarchy / nixpkgs / Flatpak updates";
Service = {
Type = "oneshot";
ExecStart = "${nomarchy-updates}/bin/nomarchy-updates check";
};
};
systemd.user.timers.nomarchy-updates = {
Unit.Description = "Periodic update-awareness check";
Timer = {
OnStartupSec = "2min";
OnCalendar = cfg.interval;
Persistent = true;
};
Install.WantedBy = [ "timers.target" ];
};
})
];
}