# 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 nomarchy-pull / # nomarchy-rebuild / nomarchy-home / 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; termSheet = import ./term-sheet.nix { inherit pkgs; }; 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: nomarchy-pull && nomarchy-rebuild && nomarchy-home" 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) — nomarchy-pull && nomarchy-rebuild && nomarchy-home" [ "$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 nomarchy-pull >/dev/null 2>&1; then read -rp "Run nomarchy-pull && nomarchy-rebuild && nomarchy-home? [y/N] " a if [ "$a" = y ]; then nomarchy-pull nomarchy-rebuild command -v nomarchy-home >/dev/null 2>&1 && nomarchy-home fi 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 ;; upgrade-window) # The Waybar click target. `upgrade` prompts, so it needs a terminal — # and the bar has none: it is spawned by Hyprland, whose environment has # no $TERMINAL (that is a home.sessionVariables entry, so only login # shells see it). A whole-swap's hand-written on-click that reached for # $TERMINAL expanded to nothing and the click silently did nothing # (#141). Opening our own window is the fix that cannot rot: every # caller — generated module and whole-swap alike — names one env-free # command, and a theme file stops having an opinion about terminals. # # A classed sheet like the calendar and doctor: a short y/N flow has no # business taking the whole screen, and the class is what hyprland.nix # floats it by. Smaller than either (45%x50%) — this is a prompt, not a # document. exec ${termSheet}/bin/nomarchy-term-sheet com.nomarchy.updates 45 50 \ "$0" upgrade ;; *) echo "usage: nomarchy-updates [check|status|upgrade|upgrade-window]" >&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" ]; }; }) ]; }