From 0e42763aea6704b95dd1888c948646352c8a2374 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Fri, 19 Jun 2026 19:54:42 +0100 Subject: [PATCH] feat(menu): Back on every submenu + working snapshot fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two menu improvements. Back everywhere: every list menu now ends with a "↩ Back" entry that returns one level up — power, theme, power-profile, clipboard, files, capture, not just Tools/System — so you no longer have to Esc out and reopen to back up. A `back` helper + a shared `BACK` label keep it uniform; matched exactly so it can't collide with clipboard/filename content. Esc still quits instantly. Snapshot fallback: btrfs-assistant 2.2 segfaults on 26.05 (libbtrfsutil ABI), so `nomarchy-menu snapshot` now launches nomarchy-snapshots instead — a keyboard-driven snapper browser/restore. snapper is root-only here and rofi can't run as root under Wayland, so it runs in a terminal via sudo (one password prompt) and uses fzf to pick: browse/diff (read-only), restore files (undochange), or roll back (root config only), each behind a typed-`yes` confirmation — safer than a one-click rofi rollback. btrfs-assistant stays installed for when nixpkgs fixes it. Verified: flake check clean; home generation builds and the menu passes bash -n with all Back + snapshot wiring; nomarchy-snapshots passes shellcheck (writeShellApplication) in a snapper-enabled system build. Pending an on-machine check of the restore/rollback paths. Co-Authored-By: Claude Opus 4.8 --- docs/ROADMAP.md | 23 +++++++++++--- modules/home/rofi.nix | 52 ++++++++++++++++++++----------- modules/nixos/default.nix | 65 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 115 insertions(+), 25 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 58ef260..e3497c5 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -51,6 +51,11 @@ how to override it. Items marked ✓ are shipped. one-off — any new feature that earns a menu entry must be placed in the right submenu (don't let the root creep back to a flat list), with its direct `SUPER+CTRL+` bind and self-gating as applicable. + - ✓ **Back everywhere:** every list menu now ends with a `↩ Back` entry that + returns one level up (power/theme/power-profile/clipboard/files/capture, + not just Tools/System), so you never have to Esc out and reopen. A `back` + helper + a shared `BACK` label keep it uniform; matched exactly so it can't + collide with clipboard/filename content. Esc still quits instantly. - ✓ **Menu modules from rofi plugins:** the old `calc` flow committed the expression blind (result only in the *next* menu's `-mesg`) and `qalc -t` misparsed common phrasings (`15% of 200` → `rem(15, 1 B)`, the natural- @@ -454,11 +459,19 @@ how to override it. Items marked ✓ are shipped. artifact and will crash on hardware too). The menu wiring is correct and the app is installed, but the GUI doesn't open. The snapshot *backend* (the actual snapshots) is unaffected. Fix path: a nixpkgs override aligning - its libbtrfsutil, or wait for an upstream bump; meanwhile the menu entry - is a no-op when the binary crashes. Fallback if it lingers: a rofi-based - snapshot menu (no btrfs-assistant dependency). - - Remaining (optional): a keyboard-driven rofi browse for quick glances; - boot-from-snapshot needs a systemd-boot equivalent of grub-btrfs. + its libbtrfsutil, or wait for an upstream bump. ✓ **Fallback shipped:** + the menu now launches `nomarchy-snapshots` (system-side, gated on + `nomarchy.system.snapper`) instead — a keyboard-driven snapper + browser/restore with no btrfs-assistant dependency. snapper is root-only + here (no `ALLOW_USERS`) and rofi can't run as root under Wayland, so it + runs in a terminal via `sudo` (one password prompt) and uses `fzf` to pick: + browse/diff (read-only), restore files (`undochange`), or roll back (root + config only) — each behind a typed-`yes` confirmation, which is *safer* + than the fat-fingerable one-click rofi rollback this entry originally + avoided. btrfs-assistant stays installed for when nixpkgs fixes it. + **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, diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index e87f309..2e6e39e 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -64,6 +64,14 @@ let # rofi has show-icons (on globally; passed explicitly on the icon menus). row() { printf '%s\0icon\x1f%s\n' "$1" "$2"; } + # General menu convention: every list menu ends with a "↩ Back" entry that + # returns one level up (so you never have to Esc out and reopen). `back` + # emits it with an icon; plain pick-lists append the bare label instead. + # Always matched EXACTLY ("↩ Back") so it can't collide with clipboard or + # filename content. The arrow + go-previous icon read as "go back". + BACK="↩ Back" + back() { row "$BACK" go-previous; } + case "''${1:-}" in power) choice=$( { @@ -73,8 +81,10 @@ let row "Hibernate" system-hibernate row "Reboot" system-reboot row "Shutdown" system-shutdown + back } | rofi -dmenu -show-icons -p Power) || exit 0 case "$choice" in + "$BACK") exec "$0" ;; *Lock) loginctl lock-session ;; *Logout) hyprctl dispatch exit ;; *Suspend) systemctl suspend ;; @@ -92,17 +102,19 @@ let exit 0 fi cur=$(powerprofilesctl get 2>/dev/null) - choice=$(powerprofilesctl list 2>/dev/null \ - | sed -nE 's/^[* ] ([a-z-]+):$/\1/p' \ + choice=$( { powerprofilesctl list 2>/dev/null | sed -nE 's/^[* ] ([a-z-]+):$/\1/p'; printf '%s\n' "$BACK"; } \ | rofi -dmenu -p "profile (now: $cur)") || exit 0 + [ "$choice" = "$BACK" ] && exec "$0" system [ -n "$choice" ] && powerprofilesctl set "$choice" ;; theme) - choice=$(nomarchy-theme-sync list | rofi -dmenu -p theme) || exit 0 + choice=$( { nomarchy-theme-sync list; printf '%s\n' "$BACK"; } | rofi -dmenu -p theme) || exit 0 + [ "$choice" = "$BACK" ] && exec "$0" [ -n "$choice" ] && exec nomarchy-theme-sync apply "$choice" ;; clipboard) - sel=$(cliphist list | rofi -dmenu -p clip) || exit 0 + sel=$( { cliphist list; printf '%s\n' "$BACK"; } | rofi -dmenu -p clip) || exit 0 + [ "$sel" = "$BACK" ] && exec "$0" tools printf '%s' "$sel" | cliphist decode | wl-copy ;; calc) @@ -114,10 +126,10 @@ let -calc-command "echo -n '{result}' | wl-copy" ;; files) - sel=$(fd . "$HOME" --type f --hidden --exclude .git --exclude .cache 2>/dev/null \ - | head -n 50000 \ - | sed "s|^$HOME/||" \ + sel=$( { fd . "$HOME" --type f --hidden --exclude .git --exclude .cache 2>/dev/null \ + | head -n 50000 | sed "s|^$HOME/||"; printf '%s\n' "$BACK"; } \ | rofi -dmenu -p file) || exit 0 + [ "$sel" = "$BACK" ] && exec "$0" tools [ -n "$sel" ] && exec xdg-open "$HOME/$sel" ;; emoji) @@ -145,10 +157,12 @@ let row "Region → file" applets-screenshooter row "Full screen → clipboard" camera-photo row "Full screen → file" camera-photo + back } | rofi -dmenu -show-icons -p Capture) || exit 0 dir="$HOME/Pictures/Screenshots" file="$dir/$(date +%Y%m%d-%H%M%S).png" case "$choice" in + "$BACK") exec "$0" tools ;; *"Region → clipboard"*) grim -g "$(slurp)" - | wl-copy ;; *"Region → file"*) mkdir -p "$dir"; grim -g "$(slurp)" "$file" \ && notify-send "Screenshot saved" "$file" ;; @@ -187,12 +201,14 @@ let notify-send "Night light" "Not available (nomarchy.nightlight off?)."; exit 0 ;; snapshot) - # btrfs-assistant: snapshot browse / diff / restore / rollback over - # snapper, elevating via polkit. Shipped system-side with - # nomarchy.system.snapper; self-gate so the entry no-ops elsewhere. - command -v btrfs-assistant >/dev/null 2>&1 \ - || { notify-send "Snapshots" "btrfs-assistant isn't installed (BTRFS snapshots off?)."; exit 0; } - exec btrfs-assistant ;; + # btrfs-assistant 2.2 segfaults on 26.05 (libbtrfsutil ABI), so launch + # the keyboard-driven snapper browser instead: a terminal running it + # via sudo (snapper is root-only here — one password prompt), with + # browse/diff, restore (undochange) and rollback, each confirmed. + # Self-gated to nomarchy.system.snapper, so it no-ops elsewhere. + command -v nomarchy-snapshots >/dev/null 2>&1 \ + || { notify-send "Snapshots" "Snapshot tools unavailable (nomarchy.system.snapper off?)."; exit 0; } + exec ${cfg.terminal} -e sudo nomarchy-snapshots ;; tools) # Tools submenu — utilities you invoke. Each leaf is also reachable @@ -205,9 +221,10 @@ let row "Web search" system-search row "Capture" applets-screenshooter row "Ask Claude" internet-chat - row "Back" go-previous + back } | rofi -dmenu -show-icons -p Tools) || exit 0 case "$choice" in + "$BACK") exec "$0" ;; *Calc*) exec "$0" calc ;; *Clipboard*) exec "$0" clipboard ;; *Emoji*) exec "$0" emoji ;; @@ -215,7 +232,6 @@ let *Web*) exec "$0" web ;; *Capture*) exec "$0" capture ;; *Ask*) exec "$0" ask ;; - *Back*) exec "$0" ;; esac ;; system) @@ -229,20 +245,20 @@ let row "Do Not Disturb" notification-disabled systemctl --user cat hyprsunset.service >/dev/null 2>&1 \ && row "Night light" weather-clear-night - command -v btrfs-assistant >/dev/null 2>&1 && row "Snapshots" timeshift + command -v nomarchy-snapshots >/dev/null 2>&1 && row "Snapshots" timeshift if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then row "Power profile" preferences-system-power fi - row "Back" go-previous + back } | rofi -dmenu -show-icons -p System) || exit 0 case "$choice" in + "$BACK") exec "$0" ;; *Network*) exec "$0" network ;; *Bluetooth*) exec "$0" bluetooth ;; *"Do Not Disturb"*) exec "$0" dnd ;; *"Night light"*) exec "$0" nightlight ;; *Snapshots*) exec "$0" snapshot ;; *"Power profile"*) exec "$0" power-profile ;; - *Back*) exec "$0" ;; esac ;; "") diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix index bba2875..d88816f 100644 --- a/modules/nixos/default.nix +++ b/modules/nixos/default.nix @@ -325,8 +325,69 @@ in nixos-rebuild switch --flake /etc/nixos#default "$@" '') # The desktop snapshot manager (browse / diff / restore / rollback over - # snapper, elevating via polkit) — what `nomarchy-menu snapshot` launches. - ++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant; + # snapper, elevating via polkit). Kept for when nixpkgs fixes its crash. + ++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant + # Keyboard-driven snapper browser/restore — the fallback `nomarchy-menu + # snapshot` actually launches while btrfs-assistant 2.2 segfaults on 26.05 + # (libbtrfsutil ABI). Runs as root (snapper is root-only here; the menu + # opens it in a terminal via sudo, one password prompt), fzf to pick, with + # browse/diff (read-only) and typed-`yes` confirmation before any write. + ++ lib.optional cfg.snapper.enable (pkgs.writeShellApplication { + name = "nomarchy-snapshots"; + runtimeInputs = with pkgs; [ snapper fzf gawk gnugrep less coreutils systemd ]; + text = '' + if [ "$(id -u)" -ne 0 ]; then + echo "nomarchy-snapshots must run as root (snapper needs it) — use sudo." >&2 + exit 1 + fi + + mapfile -t configs < <(snapper list-configs | awk 'NR>2 {print $1}' | grep .) + if [ "''${#configs[@]}" -eq 0 ]; then + echo "No snapper configs found." >&2; exit 1 + elif [ "''${#configs[@]}" -eq 1 ]; then + config="''${configs[0]}" + else + config=$(printf '%s\n' "''${configs[@]}" | fzf --prompt="snapper config> ") || exit 0 + fi + + while :; do + snap=$(snapper -c "$config" list \ + | fzf --header-lines=2 --prompt="[$config] pick a snapshot (Esc quits)> ") || exit 0 + num=$(awk '{print $1}' <<<"$snap") + case "$num" in ""|*[!0-9]*) continue ;; esac + + action=$(printf '%s\n' \ + "Browse changes since #$num (read-only)" \ + "Restore changed files to #$num (undochange)" \ + "Roll the system back to #$num (reboot)" \ + "↩ Back to the snapshot list" \ + | fzf --prompt="snapshot #$num> ") || exit 0 + + case "$action" in + Browse*) + snapper -c "$config" status "$num..0" | less -R || true ;; + Restore*) + read -rp "Revert files in '$config' to snapshot #$num? Type yes to confirm: " ans || continue + if [ "$ans" = yes ]; then + snapper -c "$config" undochange "$num..0" + echo "Files restored. Press enter."; read -r _ || true + fi ;; + Roll*) + if [ "$config" != root ]; then + echo "Rollback applies to the 'root' config only; use Restore for '$config'. Press enter." + read -r _ || true + else + read -rp "Roll the SYSTEM back to #$num and REBOOT now? Type yes to confirm: " ans || continue + if [ "$ans" = yes ]; then + snapper -c root rollback "$num" + echo "Rolled back — rebooting…"; systemctl reboot + fi + fi ;; + *) continue ;; + esac + done + ''; + }); # Don't let boot entries fill the ESP over the years. boot.loader.systemd-boot.configurationLimit = lib.mkDefault 10;