diff --git a/README.md b/README.md index e1bdc37..0451d31 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,8 @@ nomarchy-pull # nix flake update — refresh inputs (nomarchy, nixpkgs, … nomarchy-rebuild # system rebuild against the CURRENT lock (sudo inside; # BTRFS snapshot first when snapper is on; nvd diff after) nomarchy-home # home-manager switch — desktop only, no sudo, no lock bump + # (prints nvd + a "What changed" toast when the generation moved) +nomarchy-what-changed # plain-language last rebuild (system + desktop; --summary for one-liners) ``` `~/.nomarchy` is **your machine flake** (`system.nix` / `home.nix`). Newer diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 8192304..ec2d089 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -55,13 +55,6 @@ runtime-computed `resume_offset` into the boot cmdline, and the dev box runs exactly this layout (`/proc/cmdline` verified) but isn't Nomarchy. Steps in HARDWARE-QUEUE. Close #76 on PASS. -### 82. Generation readability (`VISION § B`) -"What changed last rebuild" in plain language: post-switch `nvd` / -`nix store diff-closures` summary (n packages up/down/added) surfaced -as a toast and/or a "What changed?" row beside System › Rollback. -Rollback lists generations but the story is incomplete. VM assert on -the diff text. - ### 83. Doctor first-boot section: pre-activate fail flag (`VISION § A`) The installer leaves `/var/log/nomarchy-hm-preactivate.log` on the target when the theme bake fails, but nothing on the installed system @@ -133,9 +126,8 @@ high-ROI, etc.) live in the journal + ROADMAP — not here.* 2026-07-10). Keep #59 commented install guidance; no `nomarchy.hardware.nvidia.*` until a hybrid maintainer + queue. -_(#80 battery health + #81 first-boot welcome shipped 2026-07-11. - Remaining promoted: generation readability, pre-activate flag, dock - life, hall of fame → **NEXT #82–#85**.)_ +_(#80–#82 shipped 2026-07-11. Remaining promoted: pre-activate flag, + dock life, hall of fame → **NEXT #83–#85**.)_ ### v1.0 pointer diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index f4c180d..09b44c0 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -19,6 +19,17 @@ Template: --- +## 2026-07-11 — #82 generation readability +- **Task:** NEXT #82 — plain-language “what changed last rebuild”. +- **Did:** `pkgs/nomarchy-what-changed` (nvd → “N added, M removed, K + updated”); wired into lifecycle post-`nomarchy-rebuild` / `nomarchy-home` + (full nvd + notify toast); System › What changed? (toast + floating + full report). `checks.what-changed` with NOMARCHY_NVD fixture. Closed #82. +- **Verified:** **V2** — checks.what-changed green; local smoke on real + HM gens (“Desktop: 1 added, 1 removed”) + fixture counts. +- **Pending:** — +- **Next suggestion:** #83 doctor pre-activate fail flag or #85 docs. + ## 2026-07-11 — #81 first-boot welcome toast - **Task:** NEXT #81 — dismissible “you're set” card (`VISION § B`). - **Did:** `pkgs/nomarchy-first-boot` + user oneshot diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d43e907..e0363b2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -376,6 +376,11 @@ Design/decision records and a running log of shipped work (items marked `hostname == nomarchy-live` (live ISO keeps its own welcome). Toggle: `nomarchy.firstBootWelcome.enable`. `checks.first-boot` gates toast / marker / re-run silence / live skip. + ✓ **Generation readability (#82, `VISION § B`):** `nomarchy-what-changed` + wraps nvd into plain-language counts (added/removed/updated); post-switch + toast from `nomarchy-rebuild` / `nomarchy-home`; System › What changed? + (toast + floating full report). `checks.what-changed` fixtures nvd via + `NOMARCHY_NVD`. - ✓ **Hibernate double-unlock:** on resume from hibernate the LUKS passphrase already gates the machine, but locking hyprlock before sleep meant a second password. Fixed by *not locking* before an encrypted diff --git a/docs/VISION.md b/docs/VISION.md index 344e3a2..a76109d 100644 --- a/docs/VISION.md +++ b/docs/VISION.md @@ -71,7 +71,7 @@ Reference: [HARDWARE.md](HARDWARE.md) §4–§5, §10. |------|--------|-------| | **Silent first-week card** ✓ | One dismissible “you’re set” (menu, themes, keys, network) — not a wizard | ✓ shipped #81 — `settings.firstBootShown` + notify; live ISO keeps its own toast | | **Boreal as default** | Identity on first boot | BACKLOG PROPOSED already | -| **Generation readability** | “What changed last rebuild” in plain language | Rollback exists; story is incomplete | +| **Generation readability** ✓ | “What changed last rebuild” in plain language | ✓ shipped #82 — nvd toast + System › What changed? | | **Post-install hints** | One-shot MOTD/notify for fwupd / fprintd when relevant | Avoid permanent nag | --- diff --git a/flake.nix b/flake.nix index 7254318..7b0c1cd 100644 --- a/flake.nix +++ b/flake.nix @@ -87,9 +87,11 @@ nomarchy-control-center = final.callPackage ./pkgs/nomarchy-control-center { }; nomarchy-battery-notify = final.callPackage ./pkgs/nomarchy-battery-notify { }; nomarchy-first-boot = final.callPackage ./pkgs/nomarchy-first-boot { }; + nomarchy-what-changed = final.callPackage ./pkgs/nomarchy-what-changed { }; nomarchy-detect-hw = final.callPackage ./pkgs/nomarchy-detect-hw { }; # pull / rebuild / home (+ legacy aliases) — also on HM so a home # switch can refresh a broken system-package nomarchy-pull. + # Includes nomarchy-what-changed (generation readability, #82). nomarchy-lifecycle = final.callPackage ./pkgs/nomarchy-lifecycle { }; }; @@ -126,6 +128,7 @@ nomarchy-control-center = pkgs.nomarchy-control-center; nomarchy-battery-notify = pkgs.nomarchy-battery-notify; nomarchy-first-boot = pkgs.nomarchy-first-boot; + nomarchy-what-changed = pkgs.nomarchy-what-changed; nomarchy-detect-hw = pkgs.nomarchy-detect-hw; default = pkgs.nomarchy-theme-sync; }; @@ -746,6 +749,71 @@ ''; }; + # Generation readability (#82): nomarchy-what-changed turns an + # nvd report into plain-language counts. Inject a fake nvd via + # NOMARCHY_NVD so the VM stays minimal (no real store closures). + what-changed = pkgs.testers.runNixOSTest { + name = "nomarchy-what-changed"; + nodes.machine = { ... }: { + environment.systemPackages = [ pkgs.nomarchy-what-changed ]; + }; + testScript = '' + machine.wait_for_unit("multi-user.target") + + # Fake nvd: args are `nvd --color never diff A B` — ignore paths. + machine.succeed( + "mkdir -p /shim && cat > /shim/fake-nvd <<'EOF'\n" + "#!/bin/sh\n" + "cat <<'R'\n" + "<<< /nix/store/old\n" + ">>> /nix/store/new\n" + "Added packages:\n" + "[A.] #1 hello 2.12\n" + "[A.] #2 cowsay 3.04\n" + "Removed packages:\n" + "[R.] #1 fortune 1.0\n" + "Version changes:\n" + "[C.] #1 bash: 5.2 -> 5.3\n" + "Closure size: 100 -> 105 (10 paths added, 5 paths removed, delta +5).\n" + "R\n" + "EOF\n" + "chmod +x /shim/fake-nvd" + ) + # Dummy before/after paths (script only needs them to exist and differ). + machine.succeed("mkdir -p /tmp/gen-a /tmp/gen-b && touch /tmp/gen-a/x /tmp/gen-b/y") + + out = machine.succeed( + "NOMARCHY_NVD=/shim/fake-nvd nomarchy-what-changed --summary " + "--diff /tmp/gen-a /tmp/gen-b" + ) + assert "2 added" in out, f"missing added count:\n{out}" + assert "1 removed" in out, f"missing removed count:\n{out}" + assert "1 updated" in out, f"missing updated count:\n{out}" + + # Identical paths → no package changes (no nvd call needed). + out2 = machine.succeed( + "NOMARCHY_NVD=/shim/fake-nvd nomarchy-what-changed --summary " + "--diff /tmp/gen-a /tmp/gen-a" + ) + assert "no package changes" in out2, f"identical paths should short-circuit:\n{out2}" + + # Empty-ish nvd report. + machine.succeed( + "cat > /shim/fake-nvd-empty <<'EOF'\n" + "#!/bin/sh\n" + "echo 'No version or selection state changes.'\n" + "echo 'Closure size: 1 -> 1 (0 paths added, 0 paths removed, delta +0).'\n" + "EOF\n" + "chmod +x /shim/fake-nvd-empty" + ) + out3 = machine.succeed( + "NOMARCHY_NVD=/shim/fake-nvd-empty nomarchy-what-changed --summary " + "--diff /tmp/gen-a /tmp/gen-b" + ) + assert "no package changes" in out3, f"empty report:\n{out3}" + ''; + }; + # Memory-pressure protection (oom.nix): earlyoom must kill a # runaway allocator BEFORE the kernel thrash point — and must not # take anything else with it. A bystander unit survives the kill, diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index 93129d4..8f74373 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -895,6 +895,20 @@ ${themeRows} # Nomarchy TUI Control Center. exec ${cfg.terminal} -e nomarchy-control-center ;; + whatchanged) + # Plain-language last rebuild (system + desktop nvd). Toast the + # one-liners; open a floating terminal for the full report when + # the user wants detail (same ghostty class pattern as Doctor). + if ! command -v nomarchy-what-changed >/dev/null 2>&1; then + notify-send "What changed" "nomarchy-what-changed not on PATH (lifecycle package?)." + exit 0 + fi + summary=$(nomarchy-what-changed --summary 2>/dev/null || true) + [ -n "$summary" ] && notify-send -a Nomarchy "What changed" "$summary" + exec ghostty --class=com.nomarchy.doctor --gtk-single-instance=false \ + -e sh -c "nomarchy-what-changed + printf '\nEnter to close.'; read -r _" ;; + rollback) # Undo, one menu away (informative + rock-stable). Desktop = # Home Manager generations: theme/config history is already one @@ -996,6 +1010,9 @@ ${themeRows} fi command -v nomarchy-snapshots >/dev/null 2>&1 && row "Snapshots" timeshift row "Rollback" edit-undo + # Plain-language last-rebuild summary (nvd; BACKLOG #82). + command -v nomarchy-what-changed >/dev/null 2>&1 \ + && row "What changed?" view-refresh command -v nomarchy-doctor >/dev/null 2>&1 && row "Doctor" utilities-system-monitor # Firmware/LVFS updates — self-gated on fwupd (default-on, but off # on VMs/headless via services.fwupd.enable=false), like Printers. @@ -1024,6 +1041,7 @@ ${themeRows} *"Auto-commit"*) exec "$0" autocommit ;; *Snapshots*) exec "$0" snapshot ;; *Rollback*) exec "$0" rollback ;; + *"What changed"*) exec "$0" whatchanged ;; *Doctor*) exec "$0" doctor ;; *Firmware*) exec "$0" firmware ;; *Fingerprint*) exec "$0" fingerprint ;; @@ -1134,7 +1152,7 @@ ${themeRows} esac ;; *) - echo "usage: nomarchy-menu [lookfeel|tools|system|power|power-profile|powermgmt|batterylimit|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|display|display-profile|printers|capture|colorpicker|keybinds|ask|dnd|nightlight|autotimezone|autocommit|vpn|snapshot|doctor|firmware|fingerprint|controlcenter|rollback]" >&2 + echo "usage: nomarchy-menu [lookfeel|tools|system|power|power-profile|powermgmt|batterylimit|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|display|display-profile|printers|capture|colorpicker|keybinds|ask|dnd|nightlight|autotimezone|autocommit|vpn|snapshot|doctor|firmware|fingerprint|controlcenter|rollback|whatchanged]" >&2 exit 64 ;; esac ''; diff --git a/pkgs/nomarchy-lifecycle/default.nix b/pkgs/nomarchy-lifecycle/default.nix index b0f7c1d..aef5a28 100644 --- a/pkgs/nomarchy-lifecycle/default.nix +++ b/pkgs/nomarchy-lifecycle/default.nix @@ -2,7 +2,7 @@ # Installed on both NixOS (systemPackages) and Home Manager so a home # switch can refresh them without waiting for a full system rebuild — # otherwise a broken nomarchy-pull can never update itself. -{ lib, writeShellScriptBin, nvd, jq, symlinkJoin }: +{ lib, writeShellScriptBin, nvd, jq, symlinkJoin, nomarchy-what-changed }: let # Shared preamble: refuse root, resolve flake path. @@ -78,6 +78,13 @@ let else echo "nomarchy-rebuild: what changed:" ${nvd}/bin/nvd diff "$before" "$after" || true + # One-line toast for the session (full report already on the terminal). + if command -v notify-send >/dev/null 2>&1 \ + && command -v nomarchy-what-changed >/dev/null 2>&1; then + body=$(nomarchy-what-changed --summary --diff "$before" "$after" 2>/dev/null \ + | sed 's/^Diff: //' || true) + [ -n "''${body:-}" ] && notify-send -a Nomarchy "System rebuild" "$body" || true + fi fi ''; @@ -87,6 +94,16 @@ let echo "nomarchy-home: no flake.nix at $flake" >&2 exit 1 fi + # Snapshot the active HM generation before the switch so we can nvd it. + hm_before="" + for d in \ + "''${XDG_STATE_HOME:-$HOME/.local/state}/nix/profiles" \ + "/nix/var/nix/profiles/per-user/$(id -un)"; do + if [ -L "$d/home-manager" ]; then + hm_before=$(readlink -f "$d/home-manager" 2>/dev/null || true) + break + fi + done log=$(mktemp) trap 'rm -f "$log"' EXIT set +e @@ -102,7 +119,27 @@ let echo "Recovery: home-manager generations (or docs/RECOVERY.md)" exit "$rc" fi - echo "nomarchy-home: desktop applied." + hm_after="" + for d in \ + "''${XDG_STATE_HOME:-$HOME/.local/state}/nix/profiles" \ + "/nix/var/nix/profiles/per-user/$(id -un)"; do + if [ -L "$d/home-manager" ]; then + hm_after=$(readlink -f "$d/home-manager" 2>/dev/null || true) + break + fi + done + if [ -n "$hm_before" ] && [ -n "$hm_after" ] && [ "$hm_before" != "$hm_after" ]; then + echo "nomarchy-home: what changed:" + ${nvd}/bin/nvd diff "$hm_before" "$hm_after" || true + if command -v notify-send >/dev/null 2>&1 \ + && command -v nomarchy-what-changed >/dev/null 2>&1; then + body=$(nomarchy-what-changed --summary --diff "$hm_before" "$hm_after" 2>/dev/null \ + | sed 's/^Diff: //' || true) + [ -n "''${body:-}" ] && notify-send -a Nomarchy "Desktop updated" "$body" || true + fi + else + echo "nomarchy-home: desktop applied." + fi ''; # Legacy aliases @@ -125,6 +162,7 @@ symlinkJoin { sys-update sys-rebuild home-update + nomarchy-what-changed ]; meta = { description = "Nomarchy machine-flake lifecycle: pull, rebuild, home"; diff --git a/pkgs/nomarchy-what-changed/default.nix b/pkgs/nomarchy-what-changed/default.nix new file mode 100644 index 0000000..f3bc681 --- /dev/null +++ b/pkgs/nomarchy-what-changed/default.nix @@ -0,0 +1,9 @@ +# Plain-language generation diff (BACKLOG #82). Wraps nvd; PATH-resolved +# notify is left to callers. NOMARCHY_NVD lets checks inject a fixture. +{ writeShellApplication, coreutils, nvd, gnugrep, gnused }: + +writeShellApplication { + name = "nomarchy-what-changed"; + runtimeInputs = [ coreutils nvd gnugrep gnused ]; + text = builtins.readFile ./nomarchy-what-changed.sh; +} diff --git a/pkgs/nomarchy-what-changed/nomarchy-what-changed.sh b/pkgs/nomarchy-what-changed/nomarchy-what-changed.sh new file mode 100644 index 0000000..ca4e904 --- /dev/null +++ b/pkgs/nomarchy-what-changed/nomarchy-what-changed.sh @@ -0,0 +1,178 @@ +# nomarchy-what-changed — plain-language "what changed last rebuild". +# Wraps nvd into a short summary (for toasts / menu) and optional full +# report. In-checkout state is not involved; this only reads Nix profiles. +# +# usage: +# nomarchy-what-changed # system + home (last two gens each) +# nomarchy-what-changed system|home # one layer +# nomarchy-what-changed --summary … # one line per layer (toast body) +# nomarchy-what-changed --diff PATH1 PATH2 +# nomarchy-what-changed --summary --diff PATH1 PATH2 +# +# NOMARCHY_NVD overrides the nvd binary (tests inject a fixture). + +set -euo pipefail + +nvd_bin="${NOMARCHY_NVD:-nvd}" +summary_only=0 +mode="" # empty | system | home | diff +diff_a="" diff_b="" + +usage() { + echo "usage: nomarchy-what-changed [--summary] [system|home]" >&2 + echo " nomarchy-what-changed [--summary] --diff " >&2 + exit 64 +} + +while [ $# -gt 0 ]; do + case "$1" in + --summary) summary_only=1; shift ;; + --diff) + mode="diff" + shift + [ $# -ge 2 ] || usage + diff_a=$1; diff_b=$2; shift 2 + ;; + system|home) mode="$1"; shift ;; + -h|--help) usage ;; + *) usage ;; + esac +done + +# One-line plain-language summary of an nvd --color never report. +# Prefer package row counts ([A.]/[R.]/[C.]); fall back to closure path delta. +summarize_report() { + local label=$1 report=$2 + local added removed changed + if printf '%s\n' "$report" | grep -q 'No version or selection state changes'; then + printf '%s: no package changes\n' "$label" + return + fi + # Count package rows (nvd tags: A added, R removed, C version/selection change). + added=$(printf '%s\n' "$report" | grep -c '\[A\.' || true) + removed=$(printf '%s\n' "$report" | grep -c '\[R\.' || true) + changed=$(printf '%s\n' "$report" | grep -c '\[C\.' || true) + # Strip trailing newline noise from grep -c on empty (already || true → 0). + added=${added:-0}; removed=${removed:-0}; changed=${changed:-0} + + if [ "$added" -eq 0 ] && [ "$removed" -eq 0 ] && [ "$changed" -eq 0 ]; then + # Closure-only deltas (sources, etc. without package names). + local paths_a paths_r + paths_a=$(printf '%s\n' "$report" \ + | sed -n 's/.*(\([0-9]*\) paths added,.*/\1/p' | head -1) + paths_r=$(printf '%s\n' "$report" \ + | sed -n 's/.*paths added, \([0-9]*\) paths removed.*/\1/p' | head -1) + paths_a=${paths_a:-0}; paths_r=${paths_r:-0} + if [ "$paths_a" -eq 0 ] && [ "$paths_r" -eq 0 ]; then + printf '%s: no package changes\n' "$label" + else + printf '%s: %s paths added, %s removed\n' "$label" "$paths_a" "$paths_r" + fi + return + fi + + local parts=() + [ "$added" -gt 0 ] && parts+=("$added added") + [ "$removed" -gt 0 ] && parts+=("$removed removed") + [ "$changed" -gt 0 ] && parts+=("$changed updated") + local joined + joined=$(printf '%s, ' "${parts[@]}") + joined=${joined%, } + printf '%s: %s\n' "$label" "$joined" +} + +run_diff() { + local before=$1 after=$2 label=$3 + if [ ! -e "$before" ] || [ ! -e "$after" ]; then + printf '%s: no previous generation to compare\n' "$label" + return 0 + fi + if [ "$(readlink -f "$before" 2>/dev/null || echo "$before")" \ + = "$(readlink -f "$after" 2>/dev/null || echo "$after")" ]; then + printf '%s: no package changes\n' "$label" + return 0 + fi + local report + report=$("$nvd_bin" --color never diff "$before" "$after" 2>/dev/null) || { + printf '%s: could not diff (is nvd installed?)\n' "$label" + return 0 + } + if [ "$summary_only" -eq 1 ]; then + summarize_report "$label" "$report" + else + printf '── %s ──\n' "$label" + printf '%s\n' "$report" + echo + summarize_report "$label" "$report" + fi +} + +# Resolve last two links for a profile stem (…/system or …/home-manager). +last_two() { + local stem=$1 + # shellcheck disable=SC2012 + ls -d "${stem}"-[0-9]*-link 2>/dev/null | sort -V | tail -n 2 +} + +system_pair() { + last_two /nix/var/nix/profiles/system +} + +home_pair() { + local d + d="${XDG_STATE_HOME:-$HOME/.local/state}/nix/profiles" + if ls -d "$d"/home-manager-[0-9]*-link >/dev/null 2>&1; then + last_two "$d/home-manager" + return + fi + d="/nix/var/nix/profiles/per-user/${USER:-$(id -un)}" + if ls -d "$d"/home-manager-[0-9]*-link >/dev/null 2>&1; then + last_two "$d/home-manager" + return + fi + return 0 +} + +pair_to_before_after() { + # stdin: 0–2 paths; sets before/after (before may be empty). + local -a links=() + while IFS= read -r line; do + [ -n "$line" ] && links+=("$line") + done + before=""; after="" + if [ "${#links[@]}" -ge 2 ]; then + before=$(readlink -f "${links[0]}") + after=$(readlink -f "${links[1]}") + elif [ "${#links[@]}" -eq 1 ]; then + after=$(readlink -f "${links[0]}") + fi +} + +if [ "$mode" = diff ]; then + run_diff "$diff_a" "$diff_b" "Diff" + exit 0 +fi + +if [ -z "$mode" ] || [ "$mode" = system ]; then + before=""; after="" + pair_to_before_after < <(system_pair) + if [ -n "$after" ] && [ -n "$before" ]; then + run_diff "$before" "$after" "System" + elif [ -n "$after" ]; then + printf 'System: only one generation on disk — rebuild once more to compare\n' + else + printf 'System: no NixOS profile found\n' + fi +fi + +if [ -z "$mode" ] || [ "$mode" = home ]; then + before=""; after="" + pair_to_before_after < <(home_pair) + if [ -n "$after" ] && [ -n "$before" ]; then + run_diff "$before" "$after" "Desktop" + elif [ -n "$after" ]; then + printf 'Desktop: only one generation on disk — switch once more to compare\n' + else + printf 'Desktop: no Home Manager profile found\n' + fi +fi