diff --git a/README.md b/README.md index ecca9f8..e87a861 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,7 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**. | `nomarchy.keyboard.devices` | `{}` | Per-device layout overrides (Hyprland `device` blocks keyed by `hyprctl devices` name) — e.g. an external keyboard with its own layout/variant | | `nomarchy.keyboard.layouts` | `[]` | Extra candidate layouts; when set, a watcher prompts (rofi) for a layout on a newly-connected keyboard and remembers it per-device | | `nomarchy.nightlight.enable` | `false` | Scheduled blue-light filter (hyprsunset) — warm at night (`.temperature`, default 4000K) between `.sunset`/`.sunrise`, no shift by day; toggle it on/off from the menu (System › Night light) or the Waybar moon/sun indicator | +| `nomarchy.updates.enable` | `false` | Passive update awareness: a background check (`.interval`, default daily) that flags when flake inputs (nixpkgs, the Nomarchy input, …) are behind upstream — and, with Flatpak on, when apps have updates (`.flatpak`) — via a Waybar indicator + notification. Never changes anything; click the indicator to run the upgrade flow | | `nomarchy.hyprland.enable` | `true` | Nomarchy's Hyprland config | | `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar | | `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b1d181e..8335cf4 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -491,10 +491,19 @@ how to override it. Items marked ✓ are shipped. **Pending an on-machine check of the restore/rollback paths.** - Remaining (optional): boot-from-snapshot needs a systemd-boot equivalent of grub-btrfs. -- **Update awareness:** updates are manual today (`sys-update`/`home-update`); - add a Waybar indicator / notification when flake inputs are stale or a new - nixpkgs rev is available (optionally with a pending-change count). Augments, - never replaces, the explicit rebuild flow. +- ✓ **Update awareness:** `nomarchy.updates.enable` (opt-in, `modules/home/ + updates.nix`) — a `nomarchy-updates` checker on a systemd user timer + (`.interval`, default daily) compares each **direct** branch-tracking flake + input (nixpkgs, the Nomarchy input, home-manager, stylix … via `git + ls-remote` vs the locked rev; offline → skipped, no false alarm) and, when + the `flatpak` CLI is present (`.flatpak`), counts Flatpak updates. A + self-gating Waybar `custom/updates` indicator (hidden until something's + available; accent `󰚰 N`, signal-refreshed) + a notification that fires only + when the count *grows* (so a daily timer never nags). Click → the upgrade + flow in a terminal (`sys-update` / `flatpak update`, each confirmed). Purely + passive — it never changes anything, augmenting the explicit rebuild flow. + Indicator is in the generated bar and both summer whole-swaps. **Pending an + on-machine check** (the ls-remote/notify/timer path needs a live session). - **"nomarchy" control center:** a single TUI/GUI front-end over the common toggles — theme, power profile, opt-in services, display, DND — built on the same `nomarchy-theme-sync` / `nomarchy.*` surface the menu already diff --git a/modules/home/default.nix b/modules/home/default.nix index e1cd503..6e08691 100644 --- a/modules/home/default.nix +++ b/modules/home/default.nix @@ -18,6 +18,7 @@ ./yazi.nix # flagship TUI file manager, themed + plugins ./osd.nix # swayosd volume/brightness OSD, themed ./nightlight.nix # scheduled blue-light filter (hyprsunset), opt-in + ./updates.nix # passive update-awareness indicator + notification, opt-in ./shell.nix # zsh + starship + bat/eza/zoxide, themed ./keys.nix # gpg-agent (fronts SSH) + pinentry-qt ./fastfetch.nix # system info with the themed Nomarchy logo diff --git a/modules/home/options.nix b/modules/home/options.nix index 807ca0c..79f3542 100644 --- a/modules/home/options.nix +++ b/modules/home/options.nix @@ -185,6 +185,32 @@ in }; }; + updates = { + enable = lib.mkEnableOption '' + passive update awareness: a background check (systemd user timer) that + compares the flake's locked inputs (nixpkgs, the Nomarchy input, …) + against upstream and — when Flatpak is enabled — counts Flatpak + updates, surfacing a Waybar indicator + a notification when something + is available. It never changes anything; you still run sys-update / + home-update / flatpak update yourself''; + + interval = lib.mkOption { + type = lib.types.str; + default = "daily"; + example = "6h"; + description = "How often to check, as a systemd OnCalendar expression."; + }; + + flatpak = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Also count available Flatpak updates when the `flatpak` CLI is + present (i.e. nomarchy.services.flatpak is on). No effect otherwise. + ''; + }; + }; + monitors = lib.mkOption { type = lib.types.listOf monitorType; default = [ ]; diff --git a/modules/home/updates.nix b/modules/home/updates.nix new file mode 100644 index 0000000..976a84c --- /dev/null +++ b/modules/home/updates.nix @@ -0,0 +1,141 @@ +# 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" ]; + }; + }) + ]; +} diff --git a/modules/home/waybar.nix b/modules/home/waybar.nix index 82e9292..e8b4ebe 100644 --- a/modules/home/waybar.nix +++ b/modules/home/waybar.nix @@ -79,7 +79,7 @@ let modules-center = [ "clock" ]; modules-right = [ "tray" "pulseaudio" "cpu" "memory" "custom/powerprofile" "custom/nightlight" ] ++ lib.optional showLanguage "hyprland/language" - ++ [ "battery" "custom/notification" ]; + ++ [ "battery" "custom/updates" "custom/notification" ]; "hyprland/workspaces" = { format = "{icon}"; @@ -141,6 +141,18 @@ let on-click = "nomarchy-nightlight toggle"; }; + # Update awareness. Self-gates: hidden unless nomarchy.updates is enabled + # AND the periodic check found something (the helper prints nothing then). + # signal 9 lets the checker refresh it instantly; click opens the upgrade + # flow in a terminal. + "custom/updates" = { + exec = "nomarchy-updates status"; + return-type = "json"; + interval = 1800; + signal = 9; + on-click = "${config.nomarchy.terminal} -e nomarchy-updates upgrade"; + }; + # swaync notification bell + Do-Not-Disturb state. `-swb` streams JSON # (text/tooltip/class) on every change, so it tracks count and DND with # no polling. Left-click toggles the panel; right-click toggles DND. @@ -212,7 +224,7 @@ let font-weight: bold; } - #tray, #pulseaudio, #cpu, #memory, #custom-powerprofile, #custom-nightlight, #language, #battery, #custom-notification { + #tray, #pulseaudio, #cpu, #memory, #custom-powerprofile, #custom-nightlight, #custom-updates, #language, #battery, #custom-notification { color: @subtext; padding: 0 10px; } @@ -220,6 +232,9 @@ let /* Night-light active → warm tone, matching the filter it represents. */ #custom-nightlight.on { color: @warn; } + /* Updates pending → accent, to draw the eye. */ + #custom-updates.available { color: @accent; } + /* The power-profile speedometer glyph renders small in its em box — size it up so it reads at a glance like the other indicators. */ #custom-powerprofile { font-size: ${toString (t.fonts.size + 3)}pt; } diff --git a/templates/downstream/home.nix b/templates/downstream/home.nix index 984820b..49e788a 100644 --- a/templates/downstream/home.nix +++ b/templates/downstream/home.nix @@ -22,6 +22,12 @@ # sunrise = "07:00"; # }; # + # nomarchy.updates = { # passive update-awareness indicator + notification + # enable = true; + # interval = "daily"; # how often to check (systemd OnCalendar) + # flatpak = true; # also count Flatpak updates when flatpak is on + # }; + # # nomarchy.monitors = [ # declarative per-output layout, hotplug-applied # { name = "eDP-1"; position = "0x0"; } # { name = "HDMI-A-1"; position = "auto-right"; } diff --git a/themes/summer-day/waybar.css b/themes/summer-day/waybar.css index af01c64..b515de0 100755 --- a/themes/summer-day/waybar.css +++ b/themes/summer-day/waybar.css @@ -49,6 +49,7 @@ window#waybar { #pulseaudio, #custom-powerprofile, #custom-nightlight, +#custom-updates, #battery, #tray, #custom-notification, diff --git a/themes/summer-day/waybar.jsonc b/themes/summer-day/waybar.jsonc index 6bce4a2..094ef74 100644 --- a/themes/summer-day/waybar.jsonc +++ b/themes/summer-day/waybar.jsonc @@ -11,7 +11,7 @@ "modules-left": ["custom/launcher", "clock", "clock#date"], "modules-center": ["hyprland/workspaces"], - "modules-right": ["pulseaudio", "custom/powerprofile", "custom/nightlight", "battery", "tray", "custom/notification", "custom/powermenu"], + "modules-right": ["pulseaudio", "custom/powerprofile", "custom/nightlight", "custom/updates", "battery", "tray", "custom/notification", "custom/powermenu"], "hyprland/workspaces": { "disable-scroll": true, @@ -86,6 +86,14 @@ "on-click": "nomarchy-nightlight toggle" }, + "custom/updates": { + "return-type": "json", + "interval": 1800, + "signal": 9, + "exec": "nomarchy-updates status", + "on-click": "sh -c '$TERMINAL -e nomarchy-updates upgrade'" + }, + "custom/notification": { "format": "{icon}", "return-type": "json", diff --git a/themes/summer-night/waybar.css b/themes/summer-night/waybar.css index 23e3ffb..6643fc3 100644 --- a/themes/summer-night/waybar.css +++ b/themes/summer-night/waybar.css @@ -55,6 +55,7 @@ window#waybar { #pulseaudio, #custom-powerprofile, #custom-nightlight, +#custom-updates, #idle_inhibitor, #battery, #custom-notification, diff --git a/themes/summer-night/waybar.jsonc b/themes/summer-night/waybar.jsonc index c4996f1..2b2f78f 100644 --- a/themes/summer-night/waybar.jsonc +++ b/themes/summer-night/waybar.jsonc @@ -11,7 +11,7 @@ "modules-left": ["custom/nomarchy", "clock", "clock#date"], "modules-center": ["hyprland/workspaces"], - "modules-right": ["idle_inhibitor", "pulseaudio", "custom/powerprofile", "custom/nightlight", "battery", "tray", "custom/notification", "custom/powermenu"], + "modules-right": ["idle_inhibitor", "pulseaudio", "custom/powerprofile", "custom/nightlight", "custom/updates", "battery", "tray", "custom/notification", "custom/powermenu"], "hyprland/workspaces": { "disable-scroll": true, @@ -93,6 +93,14 @@ "on-click": "nomarchy-nightlight toggle" }, + "custom/updates": { + "return-type": "json", + "interval": 1800, + "signal": 9, + "exec": "nomarchy-updates status", + "on-click": "sh -c '$TERMINAL -e nomarchy-updates upgrade'" + }, + "custom/notification": { "format": "{icon}", "return-type": "json",